Default sub-issue assignees from parent issues

This commit is contained in:
Dotta committed 2026-04-14 11:45:22 -05:00
1 parent df57744c77
commit f585689b68
5 files changed
+205 -29

No files matched your search

+5 -2
View File
@@ -53,6 +53,7 @@ import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/component
import { CircleDot, Plus, ArrowUpDown, Layers, Check, ChevronRight, List, Columns3, User, Search } from "lucide-react";
import { KanbanBoard } from "./KanbanBoard";
import { buildIssueTree, countDescendants } from "../lib/issue-tree";
import { buildSubIssueDefaults } from "../lib/subIssueDefaults";
import type { Issue, Project } from "@paperclipai/shared";
const ISSUE_SEARCH_DEBOUNCE_MS = 150;
@@ -494,11 +495,13 @@ export function IssuesList({
else defaults.assigneeAgentId = groupKey;
}
else if (viewState.groupBy === "parent" && groupKey !== "__no_parent") {
defaults.parentId = groupKey;
const parentIssue = issueById.get(groupKey);
if (parentIssue) Object.assign(defaults, buildSubIssueDefaults(parentIssue));
else defaults.parentId = groupKey;
}
}
return defaults;
}, [projectId, viewState.groupBy]);
}, [issueById, projectId, viewState.groupBy]);
const filterToWorkspace = useCallback((workspaceId: string) => {
updateView({ workspaces: [workspaceId] });
+37
View File
@@ -372,6 +372,43 @@ describe("NewIssueDialog", () => {
act(() => root.unmount());
});
it("submits the parent assignee when a sub-issue opens with inherited defaults", async () => {
dialogState.newIssueDefaults = {
parentId: "issue-1",
parentIdentifier: "PAP-1",
parentTitle: "Parent issue",
title: "Child issue",
projectId: "project-1",
goalId: "goal-1",
assigneeAgentId: "agent-1",
};
const { root } = renderDialog(container);
await flush();
const submitButton = Array.from(container.querySelectorAll("button"))
.find((button) => button.textContent?.includes("Create Sub-Issue"));
expect(submitButton).not.toBeUndefined();
await act(async () => {
submitButton!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flush();
expect(mockIssuesApi.create).toHaveBeenCalledWith(
"company-1",
expect.objectContaining({
title: "Child issue",
parentId: "issue-1",
goalId: "goal-1",
projectId: "project-1",
assigneeAgentId: "agent-1",
}),
);
act(() => root.unmount());
});
it("keeps the mobile dialog bounded with an internal flexible scroll region", async () => {
const { root } = renderDialog(container);
await flush();
+117
View File
@@ -0,0 +1,117 @@
import { describe, expect, it } from "vitest";
import type { ExecutionWorkspace, Issue } from "@paperclipai/shared";
import { buildSubIssueDefaults } from "./subIssueDefaults";
function makeExecutionWorkspace(overrides: Partial<ExecutionWorkspace> = {}): ExecutionWorkspace {
return {
id: "workspace-1",
companyId: "company-1",
projectId: "project-1",
projectWorkspaceId: "project-workspace-1",
sourceIssueId: null,
status: "active",
mode: "isolated_workspace",
strategyType: "git_worktree",
name: "Parent workspace",
cwd: "/tmp/workspace-1",
repoUrl: null,
baseRef: null,
branchName: "feature/pap-1",
providerType: "git_worktree",
providerRef: null,
derivedFromExecutionWorkspaceId: null,
openedAt: new Date("2026-04-07T00:00:00.000Z"),
closedAt: null,
cleanupEligibleAt: null,
cleanupReason: null,
config: null,
metadata: null,
lastUsedAt: new Date("2026-04-07T00:00:00.000Z"),
createdAt: new Date("2026-04-07T00:00:00.000Z"),
updatedAt: new Date("2026-04-07T00:00:00.000Z"),
...overrides,
};
}
function makeIssue(overrides: Partial<Issue> = {}): Issue {
return {
id: "issue-1",
identifier: "PAP-1",
companyId: "company-1",
projectId: "project-1",
projectWorkspaceId: "project-workspace-1",
goalId: "goal-1",
parentId: null,
title: "Parent issue",
description: null,
status: "todo",
priority: "medium",
assigneeAgentId: null,
assigneeUserId: null,
checkoutRunId: null,
executionRunId: null,
executionAgentNameKey: null,
executionLockedAt: null,
createdByAgentId: null,
createdByUserId: null,
issueNumber: 1,
requestDepth: 0,
billingCode: null,
assigneeAdapterOverrides: null,
executionWorkspaceId: null,
executionWorkspacePreference: "shared_workspace",
executionWorkspaceSettings: null,
currentExecutionWorkspace: null,
startedAt: null,
completedAt: null,
cancelledAt: null,
hiddenAt: null,
createdAt: new Date("2026-04-07T00:00:00.000Z"),
updatedAt: new Date("2026-04-07T00:00:00.000Z"),
...overrides,
};
}
describe("buildSubIssueDefaults", () => {
it("inherits the parent agent assignee and workspace context", () => {
const defaults = buildSubIssueDefaults(
makeIssue({
assigneeAgentId: "agent-1",
executionWorkspaceId: "workspace-1",
currentExecutionWorkspace: makeExecutionWorkspace(),
}),
);
expect(defaults).toEqual({
parentId: "issue-1",
parentIdentifier: "PAP-1",
parentTitle: "Parent issue",
projectId: "project-1",
projectWorkspaceId: "project-workspace-1",
goalId: "goal-1",
executionWorkspaceId: "workspace-1",
executionWorkspaceMode: "reuse_existing",
parentExecutionWorkspaceLabel: "Parent workspace",
assigneeAgentId: "agent-1",
});
});
it("inherits a user assignee when the parent is assigned to a user", () => {
const defaults = buildSubIssueDefaults(
makeIssue({
assigneeUserId: "user-1",
}),
);
expect(defaults).toEqual({
parentId: "issue-1",
parentIdentifier: "PAP-1",
parentTitle: "Parent issue",
projectId: "project-1",
projectWorkspaceId: "project-workspace-1",
goalId: "goal-1",
executionWorkspaceMode: "shared_workspace",
assigneeUserId: "user-1",
});
});
});
+43
View File
@@ -0,0 +1,43 @@
import type { Issue } from "@paperclipai/shared";
type SubIssueDefaultSource = Pick<
Issue,
| "id"
| "identifier"
| "title"
| "projectId"
| "projectWorkspaceId"
| "goalId"
| "executionWorkspaceId"
| "executionWorkspacePreference"
| "currentExecutionWorkspace"
| "assigneeAgentId"
| "assigneeUserId"
>;
export function buildSubIssueDefaults(issue: SubIssueDefaultSource) {
const parentExecutionWorkspaceLabel =
issue.currentExecutionWorkspace?.name
?? issue.currentExecutionWorkspace?.branchName
?? issue.currentExecutionWorkspace?.cwd
?? issue.executionWorkspaceId
?? null;
return {
parentId: issue.id,
parentIdentifier: issue.identifier ?? undefined,
parentTitle: issue.title,
...(issue.projectId ? { projectId: issue.projectId } : {}),
...(issue.projectWorkspaceId ? { projectWorkspaceId: issue.projectWorkspaceId } : {}),
...(issue.goalId ? { goalId: issue.goalId } : {}),
...(issue.executionWorkspaceId ? { executionWorkspaceId: issue.executionWorkspaceId } : {}),
...(issue.executionWorkspaceId
? { executionWorkspaceMode: "reuse_existing" }
: issue.executionWorkspacePreference
? { executionWorkspaceMode: issue.executionWorkspacePreference }
: {}),
...(parentExecutionWorkspaceLabel ? { parentExecutionWorkspaceLabel } : {}),
...(issue.assigneeAgentId ? { assigneeAgentId: issue.assigneeAgentId } : {}),
...(issue.assigneeUserId ? { assigneeUserId: issue.assigneeUserId } : {}),
};
}
+3 -27
View File
@@ -78,6 +78,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { formatIssueActivityAction } from "@/lib/activity-format";
import { buildIssuePropertiesPanelKey } from "../lib/issue-properties-panel-key";
import { resolveIssueChatTranscriptRuns } from "../lib/issueChatTranscriptRuns";
import { buildSubIssueDefaults } from "../lib/subIssueDefaults";
import {
Activity as ActivityIcon,
Archive,
@@ -749,34 +750,9 @@ export function IssueDetail() {
);
const openNewSubIssue = useCallback(() => {
if (!issue) return;
openNewIssue({
parentId: issue.id,
parentIdentifier: issue.identifier ?? undefined,
parentTitle: issue.title,
projectId: issue.projectId ?? undefined,
projectWorkspaceId: issue.projectWorkspaceId ?? undefined,
goalId: issue.goalId ?? undefined,
executionWorkspaceId: issue.executionWorkspaceId ?? undefined,
executionWorkspaceMode: issue.executionWorkspaceId ? "reuse_existing" : issue.executionWorkspacePreference ?? undefined,
parentExecutionWorkspaceLabel:
issue.currentExecutionWorkspace?.name
?? issue.currentExecutionWorkspace?.branchName
?? issue.currentExecutionWorkspace?.cwd
?? issue.executionWorkspaceId
?? undefined,
});
openNewIssue(buildSubIssueDefaults(issue));
}, [
issue?.currentExecutionWorkspace?.branchName,
issue?.currentExecutionWorkspace?.cwd,
issue?.currentExecutionWorkspace?.name,
issue?.executionWorkspaceId,
issue?.executionWorkspacePreference,
issue?.goalId,
issue?.id,
issue?.identifier,
issue?.projectId,
issue?.projectWorkspaceId,
issue?.title,
issue,
openNewIssue,
]);