diff --git a/packages/views/issues/components/table-inline-title.test.tsx b/packages/views/issues/components/table-inline-title.test.tsx index 2791798a8..3b10b4a96 100644 --- a/packages/views/issues/components/table-inline-title.test.tsx +++ b/packages/views/issues/components/table-inline-title.test.tsx @@ -55,9 +55,11 @@ function makeRow(title: string): Extract< const baseProps = { onUpdate: vi.fn(), onOpen: vi.fn(), + onCreateSubIssue: vi.fn(), onToggleParent: vi.fn(), toggleLabel: "Toggle sub-issues", renameLabel: "Rename issue", + createSubIssueLabel: "Create sub-issue", }; /** Editing state lives in the table (one editor at a time); mirror that. */ @@ -66,11 +68,13 @@ function Harness({ onOpen, onUpdate, onEditingChange, + onCreateSubIssue, }: { title: string; onOpen?: () => void; onUpdate?: (updates: unknown) => void; onEditingChange?: (editing: boolean) => void; + onCreateSubIssue?: () => void; }) { const [editing, setEditing] = useState(false); return ( @@ -84,6 +88,7 @@ function Harness({ }} onOpen={onOpen ?? baseProps.onOpen} onUpdate={onUpdate ?? baseProps.onUpdate} + onCreateSubIssue={onCreateSubIssue ?? baseProps.onCreateSubIssue} /> ); } @@ -120,6 +125,23 @@ describe("InlineTitle", () => { expect(screen.queryByRole("textbox")).toBeNull(); }); + it("opens sub-issue creation without also navigating into the issue", () => { + const onCreateSubIssue = vi.fn(); + const rowClick = vi.fn(); + render( +
+ +
, + ); + + fireEvent.click( + screen.getByRole("button", { name: "Create sub-issue" }), + ); + + expect(onCreateSubIssue).toHaveBeenCalledTimes(1); + expect(rowClick).not.toHaveBeenCalled(); + }); + it("commits on click-away without also navigating into the issue", async () => { const user = userEvent.setup({ delay: null }); const onUpdate = vi.fn(); diff --git a/packages/views/issues/components/table-view-editing.test.tsx b/packages/views/issues/components/table-view-editing.test.tsx index 088c608cd..78b37acf4 100644 --- a/packages/views/issues/components/table-view-editing.test.tsx +++ b/packages/views/issues/components/table-view-editing.test.tsx @@ -20,6 +20,7 @@ import type { Issue } from "@multica/core/types"; import { renderWithI18n } from "../../test/i18n"; import { IssueSurfaceSelectionProvider } from "../surface/selection-context"; import type { IssueSurfaceSelection } from "../surface/selection-context"; +import type { IssueCreateDefaults } from "../surface/types"; import type { ChildProgress } from "./list-row"; import { TableView, useReleaseEditingCellOnUnmount } from "./table-view"; @@ -131,10 +132,12 @@ function Harness({ issues, childProgressMap, surfaceKey, + onCreateIssue = () => {}, }: { issues: Issue[]; childProgressMap: Map; surfaceKey: string; + onCreateIssue?: (defaults: IssueCreateDefaults) => void; }) { return ( @@ -149,6 +152,7 @@ function Harness({ total={issues.length} search="" onSearchChange={() => {}} + onCreateIssue={onCreateIssue} exportIssues={() => Promise.resolve(issues)} resolveExportLookups={() => Promise.resolve({ @@ -241,6 +245,37 @@ describe("TableView cell editors under data refresh", () => { expect(screen.queryByRole("button", { name: /Backlog/ })).toBeNull(); expect(identifiers()).toEqual(["MUL-b", "MUL-a"]); }, 20_000); + + it("opens creation with the row as parent and inherits its project", async () => { + const user = userEvent.setup({ delay: null, pointerEventsCheck: 0 }); + const onCreateIssue = vi.fn(); + const issue = { + ...makeIssue("a", "Alpha task", "todo"), + project_id: "project-1", + }; + + renderWithI18n( + + + , + ); + + const row = screen.getByText("MUL-a").closest("tr")!; + await user.click( + within(row).getByRole("button", { name: "Create sub-issue" }), + ); + + expect(onCreateIssue).toHaveBeenCalledWith({ + parent_issue_id: "a", + parent_issue_identifier: "MUL-a", + project_id: "project-1", + }); + }); }); // Row virtualization unmounts a cell when its row scrolls out of the window diff --git a/packages/views/issues/components/table-view.tsx b/packages/views/issues/components/table-view.tsx index f14f42844..cdfe5dda3 100644 --- a/packages/views/issues/components/table-view.tsx +++ b/packages/views/issues/components/table-view.tsx @@ -99,6 +99,7 @@ import { ProjectPicker } from "../../projects/components/project-picker"; import { useT } from "../../i18n"; import { useIssueSurfaceActionsOptional } from "../surface/actions-context"; import { useIssueSurfaceSelection } from "../surface/selection-context"; +import type { IssueCreateDefaults } from "../surface/types"; import { ProgressRing } from "./progress-ring"; import { AssigneePicker, @@ -137,6 +138,7 @@ type TableViewProps = { total: number; search: string; onSearchChange: (query: string) => void; + onCreateIssue: (defaults: IssueCreateDefaults) => void; exportIssues: () => Promise; resolveExportLookups: (needs: { projects: boolean; @@ -476,9 +478,11 @@ export function InlineTitle({ onEditingChange, onUpdate, onOpen, + onCreateSubIssue, onToggleParent, toggleLabel, renameLabel, + createSubIssueLabel, }: { row: Extract; /** Rename state is owned by the table (one editor at a time) so it also @@ -488,9 +492,11 @@ export function InlineTitle({ onUpdate: (updates: Partial) => void; /** Navigate to the issue — clicking the title is the primary way IN. */ onOpen: () => void; + onCreateSubIssue: () => void; onToggleParent: () => void; toggleLabel: string; renameLabel: string; + createSubIssueLabel: string; }) { const [draft, setDraft] = useState(row.issue.title); const editingRef = useRef(editing); @@ -585,6 +591,17 @@ export function InlineTitle({ > {row.issue.title} +