Refactor CobHeader

This commit is contained in:
Sebastian Martinez
2023-11-21 15:15:27 +01:00
committed by Rūdolfs Ošiņš
parent ad84bc9f30
commit 2f013df4e1
6 changed files with 209 additions and 300 deletions
+2 -88
View File
@@ -1,35 +1,7 @@
<script lang="ts" strictEvents>
import { createEventDispatcher } from "svelte";
<script lang="ts">
import * as utils from "@app/lib/utils";
import IconSmall from "@app/components/IconSmall.svelte";
import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
import TextInput from "@app/components/TextInput.svelte";
import IconButton from "@app/components/IconButton.svelte";
export let preview: boolean = false;
export let mode: "readCreate" | "readWrite" | "readOnly" = "readOnly";
export let id: string | undefined = undefined;
export let title: string = "";
export let editTitle: ((title: string) => Promise<void>) | undefined =
undefined;
async function handleTitleEdit() {
if (editTitle) {
mode = "readOnly";
submitNewTitle = true;
try {
await editTitle(title);
} finally {
submitNewTitle = false;
}
}
}
const dispatch = createEventDispatcher<{ refresh: null }>();
let submitNewTitle = false;
</script>
<style>
@@ -40,17 +12,6 @@
padding: 1.5rem;
border-radius: var(--border-radius-small);
}
.title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: var(--font-size-large);
font-weight: var(--font-weight-medium);
height: 2.5rem;
}
.subtitle {
display: flex;
flex-direction: row;
@@ -72,58 +33,11 @@
margin-top: 1rem;
word-break: break-word;
}
.edit-buttons {
display: flex;
gap: 0.25rem;
}
</style>
<div class="header">
<div class="summary">
{#if (editTitle && !preview && mode === "readWrite") || (!preview && mode === "readCreate")}
<div><slot name="icon" /></div>
<TextInput
placeholder="Title"
bind:value={title}
showKeyHint={mode === "readWrite" && Boolean(id)}
on:submit={handleTitleEdit} />
{:else if title}
<div class="title">
<div><slot name="icon" /></div>
<InlineMarkdown fontSize="medium" content={title} />
</div>
{:else}
<span class="txt-missing">No title</span>
{/if}
<!-- When creating a new COB id is undefined -->
{#if editTitle && id}
<div class="edit-buttons">
{#if mode === "readWrite"}
<IconButton
title="save title"
loading={submitNewTitle}
on:click={handleTitleEdit}>
<IconSmall name={"checkmark"} />
</IconButton>
<IconButton
title="dismiss changes"
loading={submitNewTitle}
on:click={() => {
dispatch("refresh");
mode = "readOnly";
}}>
<IconSmall name={"cross"} />
</IconButton>
{:else}
<IconButton
title="edit title"
loading={submitNewTitle}
on:click={() => (mode = "readWrite")}>
<IconSmall name={"edit"} />
</IconButton>
{/if}
</div>
{/if}
<slot name="title" />
</div>
<div class="subtitle">
<slot name="state" />
+76 -34
View File
@@ -25,12 +25,14 @@
import Icon from "@app/components/Icon.svelte";
import IconButton from "@app/components/IconButton.svelte";
import IconSmall from "@app/components/IconSmall.svelte";
import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
import LabelInput from "./Cob/LabelInput.svelte";
import Layout from "./Layout.svelte";
import Markdown from "@app/components/Markdown.svelte";
import NodeId from "@app/components/NodeId.svelte";
import ReactionSelector from "@app/components/ReactionSelector.svelte";
import Reactions from "@app/components/Reactions.svelte";
import TextInput from "@app/components/TextInput.svelte";
import ThreadComponent from "@app/components/Thread.svelte";
export let baseUrl: BaseUrl;
@@ -192,7 +194,13 @@
}
}
async function editTitle(sessionId: string, title: string) {
async function editIssue(
sessionId: string,
title: string,
id: string,
body: string,
embeds: Embed[],
) {
try {
await api.project.updateIssue(
project.id,
@@ -200,12 +208,18 @@
{ type: "edit", title },
sessionId,
);
await api.project.updateIssue(
project.id,
issue.id,
{ type: "comment.edit", id, body, embeds },
sessionId,
);
} catch (error) {
if (error instanceof Error) {
modal.show({
component: ErrorModal,
props: {
title: "Issue title editing failed",
title: "Issue editing failed",
subtitle: [
"There was an error while updating the issue.",
"Check your radicle-httpd logs for details.",
@@ -374,7 +388,7 @@
let assigneeState: State = "read";
let labelState: State = "read";
let descriptionState: State = "read";
let issueState: State = "read";
</script>
<style>
@@ -407,6 +421,17 @@
flex-wrap: nowrap;
gap: 0.5rem;
}
.title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: var(--font-size-large);
font-weight: var(--font-weight-medium);
height: 2.5rem;
}
.reactions {
display: flex;
gap: 0.5rem;
@@ -440,24 +465,39 @@
<Layout {baseUrl} {project} {tracking} activeTab="issues">
<div class="issue">
<div style="display: flex; flex-direction: column; gap: 1.5rem;">
<CobHeader
id={issue.id}
title={issue.title}
editTitle={session &&
role.isDelegateOrAuthor(
session.publicKey,
project.delegates,
issue.author.id,
) &&
partial(editTitle, session.id)}
on:refresh={refreshIssue}>
<svelte:fragment slot="icon">
<div
class="state"
class:closed={issue.state.status === "closed"}
class:open={issue.state.status === "open"}>
<Icon name="issue" />
</div>
<CobHeader id={issue.id}>
<svelte:fragment slot="title">
{#if issueState !== "read"}
<div
class="state"
class:closed={issue.state.status === "closed"}
class:open={issue.state.status === "open"}>
<Icon name="issue" />
</div>
<TextInput
placeholder="Title"
bind:value={issue.title}
showKeyHint={false} />
{:else if !issue.title}
<span class="txt-missing">No title</span>
{:else}
<div class="title">
<div
class="state"
class:closed={issue.state.status === "closed"}
class:open={issue.state.status === "open"}>
<Icon name="issue" />
</div>
<InlineMarkdown fontSize="medium" content={issue.title} />
</div>
{/if}
{#if session && role.isDelegateOrAuthor(session.publicKey, project.delegates, issue.author.id) && issueState === "read"}
<IconButton
title="edit issue"
on:click={() => (issueState = "edit")}>
<IconSmall name={"edit"} />
</IconButton>
{/if}
</svelte:fragment>
<svelte:fragment slot="state">
{#if issue.state.status === "open"}
@@ -472,21 +512,30 @@
{/if}
</svelte:fragment>
<div slot="description">
{#if descriptionState !== "read"}
{#if issueState !== "read"}
<ExtendedTextarea
enableAttachments
body={issue.discussion[0].body}
submitCaption="Save"
submitInProgress={descriptionState === "submit"}
submitInProgress={issueState === "submit"}
placeholder="Leave a description"
on:close={() => (descriptionState = "read")}
on:close={() => {
issueState = "read";
void refreshIssue();
}}
on:submit={async ({ detail: { comment, embeds } }) => {
if (session) {
try {
descriptionState = "submit";
await editComment(session.id, issue.id, comment, embeds);
issueState = "submit";
await editIssue(
session.id,
issue.title,
issue.id,
comment,
embeds,
);
} finally {
descriptionState = "read";
issueState = "read";
}
}
}} />
@@ -499,13 +548,6 @@
baseUrl,
project.head,
)} />
{#if role.isDelegateOrAuthor(session?.publicKey, project.delegates, issue.author.id)}
<IconButton
title="edit description"
on:click={() => (descriptionState = "edit")}>
<IconSmall name="edit" />
</IconButton>
{/if}
</div>
{/if}
<div class="reactions">
+15 -10
View File
@@ -11,16 +11,17 @@
import AssigneeInput from "@app/views/projects/Cob/AssigneeInput.svelte";
import AuthenticationErrorModal from "@app/modals/AuthenticationErrorModal.svelte";
import NodeId from "@app/components/NodeId.svelte";
import Badge from "@app/components/Badge.svelte";
import Button from "@app/components/Button.svelte";
import CobHeader from "@app/views/projects/Cob/CobHeader.svelte";
import ErrorMessage from "@app/components/ErrorMessage.svelte";
import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
import LabelInput from "@app/views/projects/Cob/LabelInput.svelte";
import Layout from "@app/views/projects/Layout.svelte";
import Markdown from "@app/components/Markdown.svelte";
import NodeId from "@app/components/NodeId.svelte";
import TextInput from "@app/components/TextInput.svelte";
import Textarea from "@app/components/Textarea.svelte";
import Icon from "@app/components/Icon.svelte";
export let baseUrl: BaseUrl;
export let project: Project;
@@ -126,9 +127,6 @@
flex: 2;
padding-right: 1rem;
}
.open {
color: var(--color-fill-success);
}
.author {
display: flex;
align-items: center;
@@ -154,11 +152,18 @@
{#if session}
<div class="form">
<div class="editor">
<CobHeader mode="readCreate" {preview} bind:title={issueTitle}>
<svelte:fragment slot="icon">
<div class="open">
<Icon name="issue" />
</div>
<CobHeader>
<svelte:fragment slot="title">
{#if !preview}
<TextInput
placeholder="Title"
bind:value={issueTitle}
showKeyHint={false} />
{:else if issueTitle}
<InlineMarkdown fontSize="medium" content={issueTitle} />
{:else}
<span class="txt-missing">No title</span>
{/if}
</svelte:fragment>
<svelte:fragment slot="state">
{#if preview}
+73 -79
View File
@@ -63,6 +63,7 @@
import Icon from "@app/components/Icon.svelte";
import IconButton from "@app/components/IconButton.svelte";
import IconSmall from "@app/components/IconSmall.svelte";
import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
import LabelInput from "@app/views/projects/Cob/LabelInput.svelte";
import Layout from "@app/views/projects/Layout.svelte";
import Link from "@app/components/Link.svelte";
@@ -72,6 +73,7 @@
import Popover, { closeFocused } from "@app/components/Popover.svelte";
import Radio from "@app/components/Radio.svelte";
import RevisionComponent from "@app/views/projects/Cob/Revision.svelte";
import TextInput from "@app/components/TextInput.svelte";
export let baseUrl: BaseUrl;
export let patch: Patch;
@@ -87,41 +89,11 @@
["Convert to draft", { status: "draft" }],
];
async function editTitle(sessionId: string, title: string) {
try {
await api.project.updatePatch(
project.id,
patch.id,
{
type: "edit",
title,
target: "delegates",
},
sessionId,
);
} catch (error) {
if (error instanceof Error) {
modal.show({
component: ErrorModal,
props: {
title: "Patch title editing failed",
subtitle: [
"There was an error while updating the issue.",
"Check your radicle-httpd logs for details.",
],
error: {
message: error.message,
stack: error.stack,
},
},
});
}
} finally {
await refreshPatch();
}
}
async function editDescription(sessionId: string, description: string) {
async function editPatch(
sessionId: string,
title: string,
description: string,
) {
try {
await api.project.updatePatch(
project.id,
@@ -133,14 +105,20 @@
},
sessionId,
);
await api.project.updatePatch(
project.id,
patch.id,
{ type: "edit", title, target: "delegates" },
sessionId,
);
} catch (error) {
if (error instanceof Error) {
modal.show({
component: ErrorModal,
props: {
title: "Patch description editing failed",
title: "Patch editing failed",
subtitle: [
"There was an error while updating the issue.",
"There was an error while updating the patch.",
"Check your radicle-httpd logs for details.",
],
error: {
@@ -477,7 +455,7 @@
type State = "read" | "submit" | "edit";
let descriptionState: State = "read";
let patchState: State = "read";
let labelState: State = "read";
let revisionId: string;
@@ -562,6 +540,17 @@
border-radius: var(--border-radius-small);
height: fit-content;
}
.title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: var(--font-size-large);
font-weight: var(--font-weight-medium);
height: 2.5rem;
}
.commit-list {
border: 1px solid var(--color-border-hint);
border-radius: var(--border-radius-small);
@@ -615,12 +604,6 @@
display: flex;
width: 100%;
}
.edit-buttons {
display: flex;
margin-left: auto;
margin-top: auto;
margin-bottom: auto;
}
.diff-button-range {
font-family: var(--font-family-monospace);
font-weight: var(--font-weight-bold);
@@ -648,26 +631,43 @@
<Layout {baseUrl} {project} {tracking} activeTab="patches">
<div class="patch">
<div>
<CobHeader
id={patch.id}
title={patch.title}
editTitle={session &&
role.isDelegateOrAuthor(
session.publicKey,
project.delegates,
patch.author.id,
) &&
partial(editTitle, session.id)}
on:refresh={refreshPatch}>
<svelte:fragment slot="icon">
<div
class="state"
class:draft={patch.state.status === "draft"}
class:open={patch.state.status === "open"}
class:merged={patch.state.status === "merged"}
class:archived={patch.state.status === "archived"}>
<Icon name="patch" />
</div>
<CobHeader id={patch.id}>
<svelte:fragment slot="title">
{#if patchState !== "read"}
<div
class="state"
class:draft={patch.state.status === "draft"}
class:open={patch.state.status === "open"}
class:merged={patch.state.status === "merged"}
class:archived={patch.state.status === "archived"}>
<Icon name="patch" />
</div>
<TextInput
placeholder="Title"
bind:value={patch.title}
showKeyHint={false} />
{:else if !patch.title}
<span class="txt-missing">No title</span>
{:else}
<div class="title">
<div
class="state"
class:draft={patch.state.status === "draft"}
class:open={patch.state.status === "open"}
class:merged={patch.state.status === "merged"}
class:archived={patch.state.status === "archived"}>
<Icon name="patch" />
</div>
<InlineMarkdown fontSize="medium" content={patch.title} />
</div>
{/if}
{#if session && role.isDelegateOrAuthor(session.publicKey, project.delegates, patch.author.id) && patchState === "read"}
<IconButton
title="edit patch"
on:click={() => (patchState = "edit")}>
<IconSmall name={"edit"} />
</IconButton>
{/if}
</svelte:fragment>
<svelte:fragment slot="state">
<Badge size="small" variant={badgeColor(patch.state.status)}>
@@ -676,20 +676,23 @@
</svelte:fragment>
<svelte:fragment slot="description">
<div class="revision-description">
{#if session && descriptionState !== "read"}
{#if session && patchState !== "read"}
<ExtendedTextarea
body={newDescription}
submitCaption="Save"
submitInProgress={descriptionState === "submit"}
submitInProgress={patchState === "submit"}
placeholder="Leave a description"
on:close={() => (descriptionState = "read")}
on:close={() => {
patchState = "read";
void refreshPatch();
}}
on:submit={async ({ detail: { comment } }) => {
descriptionState = "submit";
patchState = "submit";
if (session) {
try {
await editDescription(session.id, comment);
await editPatch(session.id, patch.title, comment);
} finally {
descriptionState = "read";
patchState = "read";
}
}
}} />
@@ -704,15 +707,6 @@
{:else}
<span class="txt-missing">No description available</span>
{/if}
{#if role.isDelegateOrAuthor(session?.publicKey, project.delegates, patch.author.id) && descriptionState === "read"}
<div class="edit-buttons">
<IconButton
title="edit description"
on:click={() => (descriptionState = "edit")}>
<IconSmall name={"edit"} />
</IconButton>
</div>
{/if}
</div>
</svelte:fragment>
<div class="author" slot="author">
+19 -46
View File
@@ -48,11 +48,11 @@ test("test issue editing failing", async ({ page, authenticatedPeer }) => {
await expect(page.getByText("Comment creation failed")).toBeVisible();
});
test("edit title", async ({ page, authenticatedPeer }) => {
test("edit issue", async ({ page, authenticatedPeer }) => {
await page.goto(authenticatedPeer.uiUrl());
const { rid, projectFolder } = await createProject(
authenticatedPeer,
"edit-title",
"edit-issue",
);
await authenticatedPeer.rad(
[
@@ -61,78 +61,51 @@ test("edit title", async ({ page, authenticatedPeer }) => {
"--title",
"This is an issue to edit its title",
"--description",
"We'll give it a title and edit it.",
"We'll give it a description and edit it.",
],
{ cwd: projectFolder },
);
await page.goto(
`${authenticatedPeer.uiUrl()}/${rid}/issues/616e240787b6527780636caae581ca9975060733`,
`${authenticatedPeer.uiUrl()}/${rid}/issues/df476a2b747a782c20991a258bfb7fc726cb4b0b`,
);
await expect(
page.getByText("This is an issue to edit its title"),
).toBeVisible();
await expect(page.getByPlaceholder("Title")).toBeHidden();
await page.getByRole("button", { name: "edit title" }).click();
await page
.getByPlaceholder("Title")
.fill("This is a modified issue title to be dismissed");
await page.getByRole("button", { name: "dismiss changes" }).click();
await expect(
page.getByText("This is an issue to edit its title"),
).toBeVisible();
await page.getByRole("button", { name: "edit title" }).click();
await page.getByPlaceholder("Title").fill("This is a modified issue title");
await page.getByRole("button", { name: "save title" }).click();
await expect(page.getByRole("button", { name: "save title" })).toBeHidden();
await page.reload();
await expect(page.getByText("This is a modified issue title")).toBeVisible();
});
test("edit description", async ({ page, authenticatedPeer }) => {
await page.goto(authenticatedPeer.uiUrl());
const { rid, projectFolder } = await createProject(
authenticatedPeer,
"edit-description",
);
await authenticatedPeer.rad(
[
"issue",
"open",
"--title",
"This is an issue to edit its description",
"--description",
"We'll give it a description and edit it.",
],
{ cwd: projectFolder },
);
await page.goto(
`${authenticatedPeer.uiUrl()}/${rid}/issues/335e2823a71ac91203913a484dd771fd79f75139`,
);
await expect(
page.getByText("We'll give it a description and edit it."),
).toBeVisible();
await expect(page.getByPlaceholder("Leave a description")).toBeHidden();
await page.getByRole("button", { name: "edit description" }).click();
await page.getByRole("button", { name: "edit issue" }).click();
await page
.getByPlaceholder("Title")
.fill("This is a modified issue title to be dismissed");
await page
.getByPlaceholder("Leave a description")
.fill("This is a modified issue description to be dismissed");
await page.getByRole("button", { name: "Cancel" }).click();
await expect(
page.getByText("This is an issue to edit its title"),
).toBeVisible();
await expect(
page.getByText("We'll give it a description and edit it."),
).toBeVisible();
await page.getByRole("button", { name: "edit description" }).click();
await page.getByRole("button", { name: "edit issue" }).click();
await page.getByPlaceholder("Title").fill("This is a modified issue title");
await page
.getByPlaceholder("Leave a description")
.fill("This is a modified issue description");
await page.getByRole("button", { name: "Save" }).click();
await expect(page.getByRole("button", { name: "Save" })).toBeHidden();
await page.reload();
await expect(page.getByText("This is a modified issue title")).toBeVisible();
await expect(
page.getByText("This is a modified issue description"),
).toBeVisible();
await page.reload({ waitUntil: "networkidle" });
await expect(page.getByText("This is a modified issue title")).toBeVisible();
await expect(
page.getByText("This is a modified issue description"),
).toBeVisible();
+24 -43
View File
@@ -163,16 +163,23 @@ test("change patch state", async ({ page, authenticatedPeer }) => {
await expect(page.getByRole("button", { name: "0 patches" })).toBeVisible();
});
test("edit title", async ({ page, authenticatedPeer }) => {
test("edit patch", async ({ page, authenticatedPeer }) => {
const { rid, projectFolder } = await createProject(
authenticatedPeer,
"edit-title",
"edit-patch",
);
await authenticatedPeer.git(["switch", "-c", "edit-title"], {
await authenticatedPeer.git(["switch", "-c", "edit-patch"], {
cwd: projectFolder,
});
await authenticatedPeer.git(
["commit", "--allow-empty", "-m", "Some patch title"],
[
"commit",
"--allow-empty",
"-m",
"Some patch title",
"-m",
"This should be a description",
],
{
cwd: projectFolder,
},
@@ -185,62 +192,36 @@ test("edit title", async ({ page, authenticatedPeer }) => {
await page.goto(`${authenticatedPeer.uiUrl()}/${rid}/patches/${patchId}`);
const titleLocator = page.getByText("Some patch title").first();
const descriptionLocator = page.getByText("This should be a description");
await expect(titleLocator).toBeVisible();
await expect(descriptionLocator).toBeVisible();
await expect(page.getByPlaceholder("Title")).toBeHidden();
await expect(page.getByPlaceholder("Leave a description")).toBeHidden();
await page.getByRole("button", { name: "edit title" }).click();
await page.getByRole("button", { name: "edit patch" }).click();
await page
.getByPlaceholder("Title")
.fill("This is a modified patch title to be dismissed");
await page.getByRole("button", { name: "dismiss changes" }).click();
await expect(titleLocator).toBeVisible();
await page.getByRole("button", { name: "edit title" }).click();
await page.getByPlaceholder("Title").fill("This is a modified patch title");
await page.getByRole("button", { name: "save title" }).click();
await expect(page.getByRole("button", { name: "save title" })).toBeHidden();
await page.reload();
await expect(page.getByText("This is a modified patch title")).toBeVisible();
});
test("edit description", async ({ page, authenticatedPeer }) => {
const { rid, projectFolder } = await createProject(
authenticatedPeer,
"edit-description",
);
await authenticatedPeer.git(["switch", "-c", "edit-description"], {
cwd: projectFolder,
});
await authenticatedPeer.git(
["commit", "--allow-empty", "-m", "Some patch title"],
{
cwd: projectFolder,
},
);
const patchId = extractPatchId(
await authenticatedPeer.git(["push", "rad", "HEAD:refs/patches"], {
cwd: projectFolder,
}),
);
await page.goto(`${authenticatedPeer.uiUrl()}/${rid}/patches/${patchId}`);
await expect(page.getByText("No description available")).toBeVisible();
await expect(page.getByPlaceholder("Leave a description")).toBeHidden();
await page.getByRole("button", { name: "edit description" }).click();
await page
.getByPlaceholder("Leave a description")
.fill("This is a modified patch description to be dismissed");
await page.getByRole("button", { name: "Cancel" }).click();
await expect(page.getByText("No description available")).toBeVisible();
await expect(titleLocator).toBeVisible();
await expect(descriptionLocator).toBeVisible();
await page.getByRole("button", { name: "edit description" }).click();
await page.getByRole("button", { name: "edit patch" }).click();
await page.getByPlaceholder("Title").fill("This is a modified patch title");
await page
.getByPlaceholder("Leave a description")
.fill("This is a modified patch description");
await page.getByRole("button", { name: "Save" }).click();
await expect(page.getByRole("button", { name: "Save" })).toBeHidden();
await expect(page.getByText("This is a modified patch title")).toBeVisible();
await expect(
page.getByText("This is a modified patch description"),
).toBeVisible();
await page.reload();
await expect(page.getByText("This is a modified patch title")).toBeVisible();
await expect(
page.getByText("This is a modified patch description"),
).toBeVisible();