From 1ef0f9dfd1b223caab543b0bdb008fb675355c79 Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Mon, 4 Sep 2023 12:04:28 +0200 Subject: [PATCH] Add issue embeds Signed-off-by: Sebastian Martinez --- httpd-client/lib/fetcher.ts | 9 +- httpd-client/lib/project.ts | 1 + httpd-client/lib/project/comment.ts | 14 +-- httpd-client/lib/project/issue.ts | 7 +- httpd-client/tests/project.test.ts | 1 + httpd-client/tests/support/support.ts | 2 +- public/typography.css | 5 + src/components/Chip.svelte | 40 +++----- src/components/Clipboard.svelte | 6 ++ src/components/Comment.svelte | 1 - src/components/Markdown.svelte | 36 ++++++- src/components/Reactions.svelte | 35 +++++-- src/components/Textarea.svelte | 20 +++- src/components/Thread.svelte | 35 ++++++- src/lib/file.ts | 105 ++++++++++++++++++++ src/lib/utils.ts | 4 + src/views/projects/Cob/AssigneeInput.svelte | 37 +++++-- src/views/projects/Cob/Embeds.svelte | 52 ++++++++++ src/views/projects/Cob/ErrorModal.svelte | 3 +- src/views/projects/Cob/LabelInput.svelte | 38 ++++--- src/views/projects/Issue.svelte | 102 +++++++++++++++++-- src/views/projects/Issue/New.svelte | 31 +++++- tests/e2e/project/issues.spec.ts | 69 ++++++++++++- tests/e2e/project/patches.spec.ts | 2 +- tests/support/fixtures.ts | 8 +- tests/support/globalSetup.ts | 24 +++-- tests/support/heartwood-version | 2 +- tests/support/peerManager.ts | 3 + 28 files changed, 585 insertions(+), 107 deletions(-) create mode 100644 src/lib/file.ts create mode 100644 src/views/projects/Cob/Embeds.svelte diff --git a/httpd-client/lib/fetcher.ts b/httpd-client/lib/fetcher.ts index dc6af1044..50c6e0d4e 100644 --- a/httpd-client/lib/fetcher.ts +++ b/httpd-client/lib/fetcher.ts @@ -94,12 +94,17 @@ export class Fetcher { ): Promise> { const response = await this.fetch(params); - const responseBody = await response.json(); - if (!response.ok) { + let responseBody = await response.text(); + try { + responseBody = JSON.parse(responseBody); + } catch (_e: unknown) { + // We keep the original text response body. + } throw new ResponseError(params.method, response, responseBody); } + const responseBody = await response.json(); const result = schema.safeParse(responseBody); if (result.success) { return result.data; diff --git a/httpd-client/lib/project.ts b/httpd-client/lib/project.ts index 5bc355e54..6ef6c88c6 100644 --- a/httpd-client/lib/project.ts +++ b/httpd-client/lib/project.ts @@ -372,6 +372,7 @@ export class Client { title: string; description: string; assignees: string[]; + embeds: { name: string; content: string }[]; labels: string[]; }, authToken: string, diff --git a/httpd-client/lib/project/comment.ts b/httpd-client/lib/project/comment.ts index 078cbc202..1e2d4ce43 100644 --- a/httpd-client/lib/project/comment.ts +++ b/httpd-client/lib/project/comment.ts @@ -1,20 +1,14 @@ -import type { ZodSchema } from "zod"; +import type { z } from "zod"; import { array, number, object, string, tuple } from "zod"; -export interface Comment { - id: string; - author: { id: string; alias?: string }; - body: string; - reactions: [string, string][]; - timestamp: number; - replyTo: string | null; -} +export type Comment = z.infer; export const commentSchema = object({ id: string(), author: object({ id: string(), alias: string().optional() }), body: string(), + embeds: array(object({ name: string(), content: string() })), reactions: array(tuple([string(), string()])), timestamp: number(), replyTo: string().nullable(), -}) satisfies ZodSchema; +}); diff --git a/httpd-client/lib/project/issue.ts b/httpd-client/lib/project/issue.ts index 1b49a7d6f..5524cc93a 100644 --- a/httpd-client/lib/project/issue.ts +++ b/httpd-client/lib/project/issue.ts @@ -56,7 +56,12 @@ export type IssueUpdateAction = assignees: string[]; } | { type: "lifecycle"; state: IssueState } - | { type: "comment"; body: string; replyTo: string } + | { + type: "comment"; + body: string; + embeds: { name: string; content: string }[]; + replyTo: string; + } | { type: "comment.edit"; id: string; body: string } | { type: "comment.redact"; id: string } | { diff --git a/httpd-client/tests/project.test.ts b/httpd-client/tests/project.test.ts index 9cba55c04..85fab1ee6 100644 --- a/httpd-client/tests/project.test.ts +++ b/httpd-client/tests/project.test.ts @@ -120,6 +120,7 @@ describe("project", () => { title: "aaa", description: "bbb", assignees: [], + embeds: [], labels: ["bug", "documentation"], }, sessionId, diff --git a/httpd-client/tests/support/support.ts b/httpd-client/tests/support/support.ts index fca619636..8b2eac64c 100644 --- a/httpd-client/tests/support/support.ts +++ b/httpd-client/tests/support/support.ts @@ -11,7 +11,7 @@ export async function createIssueToBeModified( ) { const { id } = await api.project.createIssue( cobRid, - { title: "aaa", description: "bbb", assignees: [], labels: [] }, + { title: "aaa", description: "bbb", embeds: [], assignees: [], labels: [] }, sessionId, ); diff --git a/public/typography.css b/public/typography.css index 409e390f6..7eed0696d 100644 --- a/public/typography.css +++ b/public/typography.css @@ -156,3 +156,8 @@ p { margin: 0 0.05em 0 0.1em; vertical-align: -0.1em; } +.txt-overflow { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/src/components/Chip.svelte b/src/components/Chip.svelte index 51c2dd365..552fafb0c 100644 --- a/src/components/Chip.svelte +++ b/src/components/Chip.svelte @@ -1,11 +1,5 @@ -
- - - - + + - {#if removeable} - + {#if actionable} + + + {/if}
diff --git a/src/components/Clipboard.svelte b/src/components/Clipboard.svelte index 9c3d02c33..b500af77e 100644 --- a/src/components/Clipboard.svelte +++ b/src/components/Clipboard.svelte @@ -8,6 +8,7 @@ export let text: string; export let small = false; + export let tiny = false; export let tooltip: string | undefined = undefined; const dispatch = createEventDispatcher<{ copied: null }>(); @@ -41,6 +42,10 @@ width: 1.5rem; height: 1.5rem; } + .clipboard.tiny { + width: 1rem; + height: 1rem; + } .clipboard:hover :global(svg) { fill: var(--color-foreground); } @@ -55,6 +60,7 @@ title={tooltip} class="clipboard" class:small + class:tiny on:click|stopPropagation={copy}> diff --git a/src/components/Comment.svelte b/src/components/Comment.svelte index c88703f29..d6b313aa9 100644 --- a/src/components/Comment.svelte +++ b/src/components/Comment.svelte @@ -123,7 +123,6 @@
{ if (id) { dispatch("react", { id, ...event.detail }); diff --git a/src/components/Markdown.svelte b/src/components/Markdown.svelte index a57fe5073..ed86df819 100644 --- a/src/components/Markdown.svelte +++ b/src/components/Markdown.svelte @@ -8,13 +8,25 @@ import markdown from "@app/lib/markdown"; import { Renderer } from "@app/lib/markdown"; import { highlight } from "@app/lib/syntax"; - import { isUrl, twemoji, scrollIntoView, canonicalize } from "@app/lib/utils"; + import { + isUrl, + twemoji, + scrollIntoView, + canonicalize, + isCommit, + } from "@app/lib/utils"; + import { mimes } from "@app/lib/file"; export let content: string; // If present, resolve all relative links with respect to this URL export let linkBaseUrl: string | undefined = undefined; export let path: string = "/"; export let rawPath: string | undefined = undefined; + // If present, means we are in a preview context, + // use this for image previews instead of /raw URLs. + export let embeds: + | Map + | undefined = undefined; $: doc = matter(content); $: frontMatter = Object.entries(doc.data).filter( @@ -73,6 +85,28 @@ for (const i of container.querySelectorAll("img")) { const imagePath = i.getAttribute("src"); + // If the image is an oid embed + if (imagePath && isCommit(imagePath)) { + const embed = embeds?.get(imagePath); + if (embed) { + const fileExtension = embed.name.split(".").pop(); + if (fileExtension) { + i.setAttribute("src", embed.content); + } + } else { + const fileExtension = i.alt.split(".").pop(); + const url = new URL(rawPath); + // If a user changes the alt text of an image, + // the browser is still able to infer the mime type. + if (fileExtension && fileExtension in mimes) { + url.search = `?mime=${mimes[fileExtension]}`; + } + url.pathname = canonicalize(`blobs/${imagePath}`, url.pathname); + i.setAttribute("src", url.toString()); + } + continue; + } + // Make sure the source isn't a URL before trying to fetch it from the repo if ( imagePath && diff --git a/src/components/Reactions.svelte b/src/components/Reactions.svelte index 605cbde68..12d3bd243 100644 --- a/src/components/Reactions.svelte +++ b/src/components/Reactions.svelte @@ -2,9 +2,9 @@ import { createEventDispatcher } from "svelte"; import Chip from "@app/components/Chip.svelte"; + import { httpdStore } from "@app/lib/httpd"; export let reactions: Map; - export let clickable: boolean = false; const dispatch = createEventDispatcher<{ remove: { nids: string[]; reaction: string }; @@ -22,22 +22,35 @@ flex-direction: row; gap: 0.5rem; } + .close { + color: inherit; + border: none; + border-bottom-right-radius: var(--border-radius); + border-top-right-radius: var(--border-radius); + background-color: transparent; + line-height: 1.5; + padding: 0; + cursor: pointer; + } + .close:hover { + color: var(--color-foreground); + }
- {#each reactions as [reaction, nids], key} - { - if (clickable) { - dispatch("remove", { nids, reaction }); - } - }}> -
+ {#each reactions as [reaction, nids]} + +
{reaction} {nids.length}
+
+ +
{/each}
diff --git a/src/components/Textarea.svelte b/src/components/Textarea.svelte index 10b3b041c..d63d7312e 100644 --- a/src/components/Textarea.svelte +++ b/src/components/Textarea.svelte @@ -1,11 +1,14 @@ + + + +
+
+ Attachments +
+
+ {#each embeds as embed} + +
+ {embed.name} +
+ +
+ {:else} +
No attachments
+ {/each} +
+
diff --git a/src/views/projects/Cob/ErrorModal.svelte b/src/views/projects/Cob/ErrorModal.svelte index c49bc7545..0ca0d85e8 100644 --- a/src/views/projects/Cob/ErrorModal.svelte +++ b/src/views/projects/Cob/ErrorModal.svelte @@ -4,7 +4,8 @@ export let title: string; export let subtitle: string[]; - export let error: Error; + // This is more explicit than the standard error type. + export let error: { message: string; stack?: string }; diff --git a/src/views/projects/Cob/LabelInput.svelte b/src/views/projects/Cob/LabelInput.svelte index dd63515a3..f135c6065 100644 --- a/src/views/projects/Cob/LabelInput.svelte +++ b/src/views/projects/Cob/LabelInput.svelte @@ -24,7 +24,7 @@ } else { updatedLabels = [...updatedLabels, sanitizedValue]; inputValue = ""; - if (action === "create") { + if (action === "create" || action === "edit") { dispatch("save", updatedLabels); } } @@ -33,9 +33,9 @@ } } - function removeLabel({ detail: key }: { detail: number }) { - updatedLabels = updatedLabels.filter((_, i) => i !== key); - if (action === "create") { + function removeLabel(remove: string) { + updatedLabels = updatedLabels.filter(label => label !== remove); + if (action === "create" || action === "edit") { dispatch("save", updatedLabels); } } @@ -57,10 +57,18 @@ gap: 0.5rem; margin-bottom: 1.25rem; } - .label { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + .close { + color: inherit; + border: none; + border-bottom-right-radius: var(--border-radius); + border-top-right-radius: var(--border-radius); + background-color: transparent; + line-height: 1.5; + padding: 0; + cursor: pointer; + } + .close:hover { + color: var(--color-foreground); } @@ -91,12 +99,14 @@ {/if}