diff --git a/web/src/lib/components/repo/issues/IssueBody.stories.svelte b/web/src/lib/components/repo/issues/IssueBody.stories.svelte new file mode 100644 index 00000000..87d7b9f9 --- /dev/null +++ b/web/src/lib/components/repo/issues/IssueBody.stories.svelte @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/web/src/lib/components/repo/issues/IssueBody.svelte b/web/src/lib/components/repo/issues/IssueBody.svelte new file mode 100644 index 00000000..5fd52ee1 --- /dev/null +++ b/web/src/lib/components/repo/issues/IssueBody.svelte @@ -0,0 +1,17 @@ + + +{#if bodyHtml} + +
{@html bodyHtml}
+{:else if body} +
{body}
+{/if} diff --git a/web/src/lib/components/repo/issues/IssueHeader.stories.svelte b/web/src/lib/components/repo/issues/IssueHeader.stories.svelte new file mode 100644 index 00000000..6b7fdae5 --- /dev/null +++ b/web/src/lib/components/repo/issues/IssueHeader.stories.svelte @@ -0,0 +1,27 @@ + + + + diff --git a/web/src/lib/components/repo/issues/IssueHeader.svelte b/web/src/lib/components/repo/issues/IssueHeader.svelte new file mode 100644 index 00000000..5a176365 --- /dev/null +++ b/web/src/lib/components/repo/issues/IssueHeader.svelte @@ -0,0 +1,16 @@ + + +
+

+ {title} + #{rkey} +

+
diff --git a/web/src/lib/components/repo/issues/IssueInfoBar.stories.svelte b/web/src/lib/components/repo/issues/IssueInfoBar.stories.svelte new file mode 100644 index 00000000..dd66c199 --- /dev/null +++ b/web/src/lib/components/repo/issues/IssueInfoBar.stories.svelte @@ -0,0 +1,28 @@ + + + + diff --git a/web/src/lib/components/repo/issues/IssueInfoBar.svelte b/web/src/lib/components/repo/issues/IssueInfoBar.svelte new file mode 100644 index 00000000..5316c496 --- /dev/null +++ b/web/src/lib/components/repo/issues/IssueInfoBar.svelte @@ -0,0 +1,35 @@ + + +
+ + + + opened by + + + + + + + + {#if actions} +
+ {@render actions()} +
+ {/if} +
diff --git a/web/src/lib/components/repo/issues/IssueLabelPanel.stories.svelte b/web/src/lib/components/repo/issues/IssueLabelPanel.stories.svelte new file mode 100644 index 00000000..3b035df3 --- /dev/null +++ b/web/src/lib/components/repo/issues/IssueLabelPanel.stories.svelte @@ -0,0 +1,13 @@ + + + + diff --git a/web/src/lib/components/repo/issues/IssueLabelPanel.svelte b/web/src/lib/components/repo/issues/IssueLabelPanel.svelte new file mode 100644 index 00000000..59a20321 --- /dev/null +++ b/web/src/lib/components/repo/issues/IssueLabelPanel.svelte @@ -0,0 +1,6 @@ + + + diff --git a/web/src/lib/components/repo/issues/IssueList.stories.svelte b/web/src/lib/components/repo/issues/IssueList.stories.svelte new file mode 100644 index 00000000..e807c130 --- /dev/null +++ b/web/src/lib/components/repo/issues/IssueList.stories.svelte @@ -0,0 +1,41 @@ + + + + diff --git a/web/src/lib/components/repo/issues/IssueList.svelte b/web/src/lib/components/repo/issues/IssueList.svelte new file mode 100644 index 00000000..5a17a998 --- /dev/null +++ b/web/src/lib/components/repo/issues/IssueList.svelte @@ -0,0 +1,23 @@ + + +{#if issues.length === 0} + +{:else} +
+ {#each issues as issue (issue.rkey)} + + {/each} +
+{/if} diff --git a/web/src/lib/components/repo/issues/IssueSearch.stories.svelte b/web/src/lib/components/repo/issues/IssueSearch.stories.svelte new file mode 100644 index 00000000..14c31f9a --- /dev/null +++ b/web/src/lib/components/repo/issues/IssueSearch.stories.svelte @@ -0,0 +1,17 @@ + + + + +
+ +
+
diff --git a/web/src/lib/components/repo/issues/IssueSearch.svelte b/web/src/lib/components/repo/issues/IssueSearch.svelte new file mode 100644 index 00000000..9b7bbfdc --- /dev/null +++ b/web/src/lib/components/repo/issues/IssueSearch.svelte @@ -0,0 +1,42 @@ + + +
+ +
+
+
diff --git a/web/src/lib/components/repo/issues/IssueStatePill.stories.svelte b/web/src/lib/components/repo/issues/IssueStatePill.stories.svelte new file mode 100644 index 00000000..c3360fb4 --- /dev/null +++ b/web/src/lib/components/repo/issues/IssueStatePill.stories.svelte @@ -0,0 +1,22 @@ + + + + diff --git a/web/src/lib/components/repo/issues/IssueStatePill.svelte b/web/src/lib/components/repo/issues/IssueStatePill.svelte new file mode 100644 index 00000000..adce3822 --- /dev/null +++ b/web/src/lib/components/repo/issues/IssueStatePill.svelte @@ -0,0 +1,38 @@ + + + + + + diff --git a/web/src/lib/components/repo/issues/IssueToolbar.stories.svelte b/web/src/lib/components/repo/issues/IssueToolbar.stories.svelte new file mode 100644 index 00000000..f3402379 --- /dev/null +++ b/web/src/lib/components/repo/issues/IssueToolbar.stories.svelte @@ -0,0 +1,28 @@ + + + + diff --git a/web/src/lib/components/repo/issues/IssueToolbar.svelte b/web/src/lib/components/repo/issues/IssueToolbar.svelte new file mode 100644 index 00000000..24c527a8 --- /dev/null +++ b/web/src/lib/components/repo/issues/IssueToolbar.svelte @@ -0,0 +1,64 @@ + + + +
+
+ +
+ + + + + + + + + +
diff --git a/web/src/routes/[handle]/[repo]/issues/[aturi]/+page.svelte b/web/src/routes/[handle]/[repo]/issues/[aturi]/+page.svelte new file mode 100644 index 00000000..97a09a31 --- /dev/null +++ b/web/src/routes/[handle]/[repo]/issues/[aturi]/+page.svelte @@ -0,0 +1,177 @@ + + + + {issue.title} · Issue #{issue.rkey} · {data.repo.ownerHandle}/{data.repo.name} + · Tangled + + +{#snippet issueActions()} + + +{/snippet} + +
+ +
+
+ {#if editing} + (editing = false)} + /> + {:else} + + + + {#if deleteError} +
+ +
+ {/if} + {/if} +
+ +
+ + + {#if auth.currentUser} + + {:else} + + {/if} +
+
+ + +
+ +
+
diff --git a/web/src/routes/[handle]/[repo]/issues/[aturi]/+page.ts b/web/src/routes/[handle]/[repo]/issues/[aturi]/+page.ts new file mode 100644 index 00000000..83ccd604 --- /dev/null +++ b/web/src/routes/[handle]/[repo]/issues/[aturi]/+page.ts @@ -0,0 +1,82 @@ +import { error } from "@sveltejs/kit"; +import { createBobbinClient } from "$lib/api/client"; +import { IdentityCache } from "$lib/api/identity"; +import { getIssue, listComments, listIssueStates } from "$lib/api/records"; +import { didFromUri, rkeyFromUri } from "$lib/api/uri"; +import { renderMarkup } from "$lib/markup"; +import { buildCommentThreads, type ThreadInput } from "$lib/components/comment/comments"; +import type { PageLoad } from "./$types"; + +export const load: PageLoad = async (event) => { + const parent = await event.parent(); + const uri = decodeURIComponent(event.params.aturi); + + if (!uri.startsWith("at://")) error(404, "Issue not found"); + + const ctx = createBobbinClient({ serviceUrl: parent.publicConfig.bobbinUrl, fetch: event.fetch }); + + const record = await getIssue(ctx, uri).catch(() => null); + if (!record) error(404, "Issue not found"); + + const authorDid = didFromUri(record.uri); + const identities = new IdentityCache(ctx); + const markupOpts = { + repo: `${parent.repo.ownerHandle}/${parent.repo.name}`, + ref: parent.repo.defaultBranch, + host: event.url.host + }; + + const [author, states, comments] = await Promise.all([ + identities.resolve(authorDid).catch(() => null), + listIssueStates(ctx, record.uri, { limit: 1, order: "desc" }).catch(() => null), + listComments(ctx, record.uri, { order: "asc", limit: 100 }).catch(() => null) + ]); + + const latest = states?.items[0]?.value.state; + const state = latest?.endsWith(".closed") ? ("closed" as const) : ("open" as const); + + const body = record.value.body ?? ""; + const bodyHtml = body ? await renderMarkup(body, markupOpts) : null; + + const commentItems = comments?.items ?? []; + const threadInputs: ThreadInput[] = await Promise.all( + commentItems.map(async (item): Promise => { + const commentDid = didFromUri(item.uri); + const commentBody = item.value.body?.text ?? ""; + const [doc, commentBodyHtml] = await Promise.all([ + identities.resolve(commentDid).catch(() => null), + commentBody ? renderMarkup(commentBody, markupOpts) : Promise.resolve(null) + ]); + return { + comment: { + uri: item.uri, + cid: item.cid, + rkey: rkeyFromUri(item.uri), + authorDid: commentDid, + authorHandle: doc?.handle ?? commentDid, + createdAt: item.value.createdAt, + body: commentBody, + bodyHtml: commentBodyHtml + }, + replyTo: item.value.replyTo?.uri ?? null + }; + }) + ); + + return { + issue: { + uri: record.uri, + cid: record.cid, + rkey: rkeyFromUri(record.uri), + title: record.value.title, + body, + bodyHtml, + state, + authorDid, + authorHandle: author?.handle ?? authorDid, + createdAt: record.value.createdAt + }, + comments: buildCommentThreads(threadInputs), + markup: markupOpts + }; +};