mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 18:19:05 +02:00
Default sub-issue assignees from parent issues
This commit is contained in:
1 parent
df57744c77
commit
f585689b68
5 files changed
+205
-29
No files matched your search
@@ -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] });
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 } : {}),
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
|
||||
Reference in new issue
Block a user