diff --git a/ui/src/components/IssuesList.test.tsx b/ui/src/components/IssuesList.test.tsx index e87fa41c8ba..03f78feb98d 100644 --- a/ui/src/components/IssuesList.test.tsx +++ b/ui/src/components/IssuesList.test.tsx @@ -226,6 +226,79 @@ describe("IssuesList", () => { }); }); + it("keeps server-side search scoped to the provided parent issue filters", async () => { + const localIssue = createIssue({ id: "issue-local", identifier: "PAP-1", title: "Local issue" }); + const serverIssue = createIssue({ id: "issue-server", identifier: "PAP-2", title: "Server result" }); + + mockIssuesApi.list.mockResolvedValue([serverIssue]); + + const { root } = renderWithQueryClient( + undefined} + />, + container, + ); + + await waitForAssertion(() => { + expect(mockIssuesApi.list).toHaveBeenCalledWith("company-1", { + q: "server", + projectId: undefined, + parentId: "parent-1", + }); + expect(container.textContent).toContain("Server result"); + expect(container.textContent).not.toContain("Local issue"); + }); + + act(() => { + root.unmount(); + }); + }); + + it("uses the supplied create defaults and label for sub-issue lists", async () => { + const { root } = renderWithQueryClient( + undefined} + />, + container, + ); + + await waitForAssertion(() => { + const button = Array.from(container.querySelectorAll("button")).find( + (candidate) => candidate.textContent?.includes("New Sub-issue"), + ); + expect(button).not.toBeUndefined(); + }); + + await act(async () => { + const button = Array.from(container.querySelectorAll("button")).find( + (candidate) => candidate.textContent?.includes("New Sub-issue"), + ); + button?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + await Promise.resolve(); + }); + + expect(dialogState.openNewIssue).toHaveBeenCalledWith({ + parentId: "parent-1", + projectId: "project-1", + }); + + act(() => { + root.unmount(); + }); + }); + it("debounces search updates so typing does not notify the page on every keystroke", async () => { vi.useFakeTimers(); diff --git a/ui/src/components/IssuesList.tsx b/ui/src/components/IssuesList.tsx index 2c940ca26da..b7c7aab0407 100644 --- a/ui/src/components/IssuesList.tsx +++ b/ui/src/components/IssuesList.tsx @@ -119,6 +119,7 @@ interface Agent { } type ProjectOption = Pick & Partial>; +type IssueListRequestFilters = NonNullable[1]>; interface IssuesListProps { issues: Issue[]; @@ -132,9 +133,9 @@ interface IssuesListProps { issueLinkState?: unknown; initialAssignees?: string[]; initialSearch?: string; - searchFilters?: { - participantAgentId?: string; - }; + searchFilters?: Omit; + baseCreateIssueDefaults?: Record; + createIssueLabel?: string; enableRoutineVisibilityFilter?: boolean; onSearchChange?: (search: string) => void; onUpdateIssue: (id: string, data: Record) => void; @@ -215,6 +216,8 @@ export function IssuesList({ initialAssignees, initialSearch, searchFilters, + baseCreateIssueDefaults, + createIssueLabel, enableRoutineVisibilityFilter = false, onSearchChange, onUpdateIssue, @@ -485,8 +488,8 @@ export function IssuesList({ }, [filtered, viewState.groupBy, agents, agentName, currentUserId, workspaceNameMap, issueTitleMap]); const newIssueDefaults = useCallback((groupKey?: string) => { - const defaults: Record = {}; - if (projectId) defaults.projectId = projectId; + const defaults: Record = { ...(baseCreateIssueDefaults ?? {}) }; + if (projectId && defaults.projectId === undefined) defaults.projectId = projectId; if (groupKey) { if (viewState.groupBy === "status") defaults.status = groupKey; else if (viewState.groupBy === "priority") defaults.priority = groupKey; @@ -501,7 +504,13 @@ export function IssuesList({ } } return defaults; - }, [currentUserId, issueById, projectId, viewState.groupBy]); + }, [baseCreateIssueDefaults, currentUserId, issueById, projectId, viewState.groupBy]); + + const createActionLabel = createIssueLabel ? `Create ${createIssueLabel}` : "Create Issue"; + const createButtonLabel = createIssueLabel ? `New ${createIssueLabel}` : "New Issue"; + const openCreateIssueDialog = useCallback((groupKey?: string) => { + openNewIssue(newIssueDefaults(groupKey)); + }, [newIssueDefaults, openNewIssue]); const filterToWorkspace = useCallback((workspaceId: string) => { updateView({ workspaces: [workspaceId] }); @@ -533,9 +542,9 @@ export function IssuesList({ {/* Toolbar */}
- openNewIssue(newIssueDefaults())} + action={createActionLabel} + onAction={() => openCreateIssueDialog()} /> )} @@ -710,7 +719,7 @@ export function IssuesList({ variant="ghost" size="icon-xs" className="ml-auto text-muted-foreground" - onClick={() => openNewIssue(newIssueDefaults(group.key))} + onClick={() => openCreateIssueDialog(group.key)} > diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index c80a4d11aed..43e0e163fef 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -58,6 +58,7 @@ import { InlineEditor } from "../components/InlineEditor"; import { IssueChatThread, type IssueChatComposerHandle } from "../components/IssueChatThread"; import { useLiveRunTranscripts } from "../components/transcript/useLiveRunTranscripts"; import { IssueDocumentsSection } from "../components/IssueDocumentsSection"; +import { IssuesList } from "../components/IssuesList"; import { IssueProperties } from "../components/IssueProperties"; import { IssueWorkspaceCard } from "../components/IssueWorkspaceCard"; import type { MentionOption } from "../components/MarkdownEditor"; @@ -88,7 +89,6 @@ import { Copy, EyeOff, Hexagon, - ListTree, MessageSquare, MoreHorizontal, MoreVertical, @@ -1012,6 +1012,26 @@ export function IssueDetail() { updateIssue.mutate(data); }, [updateIssue.mutate]); + const updateChildIssue = useMutation({ + mutationFn: ({ id, data }: { id: string; data: Record }) => issuesApi.update(id, data), + onSuccess: () => { + if (resolvedCompanyId) { + queryClient.invalidateQueries({ queryKey: ["issues", resolvedCompanyId] }); + queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(resolvedCompanyId) }); + } + }, + onError: (err) => { + pushToast({ + title: "Issue update failed", + body: err instanceof Error ? err.message : "Unable to save sub-issue changes", + tone: "error", + }); + }, + }); + const handleChildIssueUpdate = useCallback((id: string, data: Record) => { + updateChildIssue.mutate({ id, data }); + }, [updateChildIssue]); + const approvalDecision = useMutation({ mutationFn: async ({ approvalId, action }: { approvalId: string; action: "approve" | "reject" }) => { if (action === "approve") { @@ -2097,54 +2117,25 @@ export function IssueDetail() { missingBehavior="placeholder" /> - {(childIssuesLoading || childIssues.length > 0) && ( -
-
-

Sub-issues

- -
- {childIssuesLoading ? ( - - ) : ( -
- {childIssues.map((child) => ( - - rememberIssueDetailLocationState( - child.identifier ?? child.id, - resolvedIssueDetailState ?? location.state, - location.search, - )} - className="flex items-center justify-between px-3 py-2 text-sm hover:bg-accent/20 transition-colors" - > -
- - - - {child.identifier ?? child.id.slice(0, 8)} - - {child.title} -
- {child.assigneeAgentId && (() => { - const name = agentMap.get(child.assigneeAgentId)?.name; - return name - ? - : {child.assigneeAgentId.slice(0, 8)}; - })()} - - ))} -
- )} +<<<<<<< HEAD +
+
+

Sub-issues

- )} + +
- {!hasAttachments && attachmentUploadButton} - {childIssues.length === 0 && ( - - )} - - } + extraActions={!hasAttachments ? attachmentUploadButton : null} /> {attachmentsInitialLoading ? (