diff --git a/web/src/lib/components/repo/tickets/AssigneeField.svelte b/web/src/lib/components/repo/tickets/AssigneeField.svelte new file mode 100644 index 000000000..e51f4329f --- /dev/null +++ b/web/src/lib/components/repo/tickets/AssigneeField.svelte @@ -0,0 +1,135 @@ + + + + {#snippet trigger({ props })} + + {/snippet} + {#snippet option(item)} + + {item.label} + {/snippet} + diff --git a/web/src/lib/components/repo/tickets/Ticket.stories.svelte b/web/src/lib/components/repo/tickets/Ticket.stories.svelte index e6f654462..561dc2f05 100644 --- a/web/src/lib/components/repo/tickets/Ticket.stories.svelte +++ b/web/src/lib/components/repo/tickets/Ticket.stories.svelte @@ -150,3 +150,29 @@ Knot will mint service-auth to access spindle as gitrepo's authority. This way, /> {/snippet} + + { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByRole("button", { name: /more actions for/i })); + await userEvent.click(await canvas.findByRole("menuitem", { name: "Edit" })); + await userEvent.click(await canvas.findByRole("button", { name: "Assignee" })); + await userEvent.click(await canvas.findByRole("option", { name: /icyphox\.sh/ })); + await waitFor(() => + expect(canvas.getByRole("button", { name: "Assignee" })).toHaveTextContent("icyphox.sh") + ); + }} +> + {#snippet template(args)} + undefined} + /> + {/snippet} + diff --git a/web/src/lib/components/repo/tickets/Ticket.svelte b/web/src/lib/components/repo/tickets/Ticket.svelte index 77646816b..9ac125139 100644 --- a/web/src/lib/components/repo/tickets/Ticket.svelte +++ b/web/src/lib/components/repo/tickets/Ticket.svelte @@ -7,6 +7,7 @@ import ConfirmModal from "$lib/components/ui/ConfirmModal.svelte"; import ReportItems from "$lib/components/moderation/ReportItems.svelte"; import MoreMenu from "$lib/components/ui/MoreMenu.svelte"; + import Separator from "$lib/components/ui/Separator.svelte"; import TicketBody from "$lib/components/repo/tickets/TicketBody.svelte"; import TicketForm from "$lib/components/repo/tickets/TicketForm.svelte"; import TicketInfoBar from "$lib/components/repo/tickets/TicketInfoBar.svelte"; @@ -44,7 +45,7 @@ assignee?: Person; assigneeOptions?: Person[]; canEditAssignee?: boolean; - onAssigneeChange?: (did: string) => void; + onAssigneeChange?: (assignee: Person) => void; onedit?: (input: { title: string; body: string; targetBranch?: string }) => Promise; ondelete?: () => Promise; extraActions?: Snippet; @@ -95,8 +96,12 @@ title={ticket.title} body={ticket.body} targetBranch={ticket.targetBranch} - onsubmit={async (input) => { + {assignee} + {assigneeOptions} + {canEditAssignee} + onsubmit={async ({ assignee: picked, ...input }) => { await onedit(input); + if (picked?.did && picked.did !== assignee?.did) onAssigneeChange?.(picked); editing = false; }} oncancel={() => (editing = false)} @@ -142,6 +147,9 @@ Delete {/if} + {#if isAuthor && (onedit || ondelete)} + + {/if} navigator.clipboard.writeText(ticket.uri)} @@ -167,9 +175,6 @@ targetBranch={ticket.targetBranch} {targetBranchHref} {assignee} - {assigneeOptions} - {canEditAssignee} - {onAssigneeChange} openedBy {extraInfo} /> diff --git a/web/src/lib/components/repo/tickets/TicketForm.svelte b/web/src/lib/components/repo/tickets/TicketForm.svelte index bcdbe700a..f0f26a112 100644 --- a/web/src/lib/components/repo/tickets/TicketForm.svelte +++ b/web/src/lib/components/repo/tickets/TicketForm.svelte @@ -8,6 +8,8 @@ import Input from "$lib/components/ui/Input.svelte"; import MarkdownEditor from "$lib/components/ui/MarkdownEditor.svelte"; import Spinner from "$lib/components/ui/Spinner.svelte"; + import AssigneeField from "$lib/components/repo/tickets/AssigneeField.svelte"; + import type { Person } from "$lib/components/repo/tickets/TicketInfoBar.svelte"; import { type MarkupContext } from "$lib/markup"; import { createAction } from "$lib/action.svelte"; @@ -18,8 +20,16 @@ body?: string; bodyPlaceholder?: string; targetBranch?: string; + assignee?: Person; + assigneeOptions?: Person[]; + canEditAssignee?: boolean; submitLabel?: string; - onsubmit: (input: { title: string; body: string; targetBranch?: string }) => Promise; + onsubmit: (input: { + title: string; + body: string; + targetBranch?: string; + assignee?: Person; + }) => Promise; oncancel: () => void; } @@ -30,6 +40,9 @@ body: initialBody = "", bodyPlaceholder = "Describe your ticket. Markdown is supported.", targetBranch: initialTargetBranch, + assignee: initialAssignee, + assigneeOptions = [], + canEditAssignee = false, submitLabel, onsubmit, oncancel @@ -38,13 +51,14 @@ let title = $state(untrack(() => initialTitle)); let body = $state(untrack(() => initialBody)); let targetBranch = $state(untrack(() => initialTargetBranch)); + let assignee = $state(untrack(() => initialAssignee)); const submitText = $derived(submitLabel ?? (mode === "edit" ? "Save" : "Create ticket")); const submitIcon = $derived(mode === "edit" ? Pencil : CirclePlus); const save = createAction(async (event: SubmitEvent) => { event.preventDefault(); if (title.trim() === "") return; - await onsubmit({ title, body }); + await onsubmit({ title, body, assignee }); }); const canSubmit = $derived(title.trim() !== "" && !save.loading); @@ -63,6 +77,18 @@ + {#if canEditAssignee} +
+ Assignee + +
+ {/if} + {#if targetBranch}
todo: target branch selector
{/if} diff --git a/web/src/lib/components/repo/tickets/TicketInfoBar.svelte b/web/src/lib/components/repo/tickets/TicketInfoBar.svelte index 32eab1aed..4203f2bfd 100644 --- a/web/src/lib/components/repo/tickets/TicketInfoBar.svelte +++ b/web/src/lib/components/repo/tickets/TicketInfoBar.svelte @@ -8,17 +8,13 @@ {#snippet person(who: Person)} @@ -74,32 +55,6 @@ {/if} {/snippet} -{#snippet assigneePencil()} - onAssigneeChange?.(did)} - label="Assignee" - placeholder="Select an assignee" - searchPlaceholder="Filter people…" - emptyLabel="No people" - class="w-auto" - > - {#snippet trigger({ props })} -
-
- {/snippet} -
-{/snippet} -
@@ -108,9 +63,6 @@ {assignee && assignedToAuthor ? "opened by and assigned to" : "opened by"} {/if} {@render person({ did: authorDid, handle: authorHandle })} - {#if assignee && assignedToAuthor && canEditAssignee} - {@render assigneePencil()} - {/if} {#if assignee && !assignedToAuthor} @@ -118,9 +70,6 @@ assigned to {@render person(assignee)} - {#if canEditAssignee} - {@render assigneePencil()} - {/if} {/if} diff --git a/web/src/lib/components/ui/MarkdownTypeahead.svelte b/web/src/lib/components/ui/MarkdownTypeahead.svelte index 8cf3fa8c0..51d4980ef 100644 --- a/web/src/lib/components/ui/MarkdownTypeahead.svelte +++ b/web/src/lib/components/ui/MarkdownTypeahead.svelte @@ -15,8 +15,7 @@ import { filterTickets, loadTickets, - RECOMMENDATION_LIMIT, - recommendUsers, + recommendPeople, suggestUsers, type Suggestion, type TicketSuggestion @@ -90,14 +89,8 @@ return collaborators; }; - const recommendations = async () => { - const known = mentionPeople?.() ?? []; - const exclude = auth?.currentDid ?? undefined; - const participants = recommendUsers(known, exclude); - if (participants.length >= RECOMMENDATION_LIMIT) return participants; - const members = await collaboratorList().catch(() => []); - return recommendUsers([...known, ...members], exclude); - }; + const recommendations = () => + recommendPeople(mentionPeople?.() ?? [], collaboratorList, auth?.currentDid ?? undefined); const settle = async (controller: AbortController, pending: Promise) => { try { diff --git a/web/src/lib/components/ui/markdownSuggestions.ts b/web/src/lib/components/ui/markdownSuggestions.ts index 8f1ddf285..2dd7b8e75 100644 --- a/web/src/lib/components/ui/markdownSuggestions.ts +++ b/web/src/lib/components/ui/markdownSuggestions.ts @@ -34,7 +34,7 @@ export interface TicketRepo { } const SUGGESTION_LIMIT = 8; -export const RECOMMENDATION_LIMIT = 5; +const RECOMMENDATION_LIMIT = 5; const TICKET_FETCH_LIMIT = 100; export const suggestUsers = async ( @@ -74,6 +74,17 @@ export const recommendUsers = ( return picked; }; +export const recommendPeople = async ( + known: MentionPerson[], + members: () => Promise, + exclude: string | undefined +): Promise => { + const participants = recommendUsers(known, exclude); + if (participants.length >= RECOMMENDATION_LIMIT) return participants; + const others = await members().catch(() => []); + return recommendUsers([...known, ...others], exclude); +}; + const ticket = ( repo: TicketRepo, isPull: boolean, diff --git a/web/src/routes/[handle]/[repo]/pulls/[aturi]/[version]/[[range]]/PullViewPage.svelte b/web/src/routes/[handle]/[repo]/pulls/[aturi]/[version]/[[range]]/PullViewPage.svelte index 8b1bc4f43..4c082ccde 100644 --- a/web/src/routes/[handle]/[repo]/pulls/[aturi]/[version]/[[range]]/PullViewPage.svelte +++ b/web/src/routes/[handle]/[repo]/pulls/[aturi]/[version]/[[range]]/PullViewPage.svelte @@ -45,6 +45,7 @@ import { renderMarkup } from "$lib/markup"; import RepoTabs from "$lib/components/repo/RepoTabs.svelte"; import Ticket from "$lib/components/repo/tickets/Ticket.svelte"; + import type { Person } from "$lib/components/repo/tickets/TicketInfoBar.svelte"; import Card from "$lib/components/ui/Card.svelte"; import type { ResolvedPathname } from "$app/types"; import type { Did } from "@atcute/lexicons/syntax"; @@ -253,11 +254,7 @@ return people; }); - let mockAssigneeDid = $state(); - - const mockAssignee = $derived( - participants.find((person) => person.did === mockAssigneeDid) ?? pull.author - ); + let mockAssignee = $state(); const targetBranchLives = createLoad(async () => { if (!browser || !data.repo.repoDid) return false; @@ -690,10 +687,10 @@ ticket={pull} markup={data.markup} {targetBranchHref} - assignee={mockAssignee} + assignee={mockAssignee ?? pull.author} assigneeOptions={participants} canEditAssignee={canPush} - onAssigneeChange={(did) => (mockAssigneeDid = did)} + onAssigneeChange={(person) => (mockAssignee = person)} onedit={updatePull} ondelete={removePull} >