mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-18 19:50:38 +09:00
Default sub-issues to parent workspace
This commit is contained in:
parent
2775a5652b
commit
758219d53f
4 changed files with 140 additions and 4 deletions
|
|
@ -311,12 +311,37 @@ describe("NewIssueDialog", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("submits parent and goal context for sub-issues", async () => {
|
it("submits parent and goal context for sub-issues", async () => {
|
||||||
|
mockProjectsApi.list.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "project-1",
|
||||||
|
name: "Alpha",
|
||||||
|
description: null,
|
||||||
|
archivedAt: null,
|
||||||
|
color: "#445566",
|
||||||
|
executionWorkspacePolicy: {
|
||||||
|
enabled: true,
|
||||||
|
defaultMode: "shared_workspace",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
mockExecutionWorkspacesApi.list.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "workspace-1",
|
||||||
|
name: "Parent workspace",
|
||||||
|
status: "active",
|
||||||
|
branchName: "feature/pap-1",
|
||||||
|
cwd: "/tmp/workspace-1",
|
||||||
|
lastUsedAt: new Date("2026-04-06T16:00:00.000Z"),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableIsolatedWorkspaces: true });
|
||||||
dialogState.newIssueDefaults = {
|
dialogState.newIssueDefaults = {
|
||||||
parentId: "issue-1",
|
parentId: "issue-1",
|
||||||
parentIdentifier: "PAP-1",
|
parentIdentifier: "PAP-1",
|
||||||
parentTitle: "Parent issue",
|
parentTitle: "Parent issue",
|
||||||
title: "Child issue",
|
title: "Child issue",
|
||||||
projectId: "project-1",
|
projectId: "project-1",
|
||||||
|
executionWorkspaceId: "workspace-1",
|
||||||
goalId: "goal-1",
|
goalId: "goal-1",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -340,9 +365,75 @@ describe("NewIssueDialog", () => {
|
||||||
parentId: "issue-1",
|
parentId: "issue-1",
|
||||||
goalId: "goal-1",
|
goalId: "goal-1",
|
||||||
projectId: "project-1",
|
projectId: "project-1",
|
||||||
|
executionWorkspaceId: "workspace-1",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
act(() => root.unmount());
|
act(() => root.unmount());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("warns when a sub-issue stops matching the parent workspace", async () => {
|
||||||
|
mockProjectsApi.list.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "project-1",
|
||||||
|
name: "Alpha",
|
||||||
|
description: null,
|
||||||
|
archivedAt: null,
|
||||||
|
color: "#445566",
|
||||||
|
executionWorkspacePolicy: {
|
||||||
|
enabled: true,
|
||||||
|
defaultMode: "shared_workspace",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
mockExecutionWorkspacesApi.list.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "workspace-1",
|
||||||
|
name: "Parent workspace",
|
||||||
|
status: "active",
|
||||||
|
branchName: "feature/pap-1",
|
||||||
|
cwd: "/tmp/workspace-1",
|
||||||
|
lastUsedAt: new Date("2026-04-06T16:00:00.000Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "workspace-2",
|
||||||
|
name: "Other workspace",
|
||||||
|
status: "active",
|
||||||
|
branchName: "feature/pap-2",
|
||||||
|
cwd: "/tmp/workspace-2",
|
||||||
|
lastUsedAt: new Date("2026-04-06T16:01:00.000Z"),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableIsolatedWorkspaces: true });
|
||||||
|
dialogState.newIssueDefaults = {
|
||||||
|
parentId: "issue-1",
|
||||||
|
parentIdentifier: "PAP-1",
|
||||||
|
parentTitle: "Parent issue",
|
||||||
|
title: "Child issue",
|
||||||
|
projectId: "project-1",
|
||||||
|
executionWorkspaceId: "workspace-1",
|
||||||
|
parentExecutionWorkspaceLabel: "Parent workspace",
|
||||||
|
goalId: "goal-1",
|
||||||
|
};
|
||||||
|
|
||||||
|
const { root } = renderDialog(container);
|
||||||
|
await flush();
|
||||||
|
|
||||||
|
expect(container.textContent).not.toContain("will no longer use the parent issue workspace");
|
||||||
|
|
||||||
|
const selects = Array.from(container.querySelectorAll("select"));
|
||||||
|
const modeSelect = selects[0] as HTMLSelectElement | undefined;
|
||||||
|
expect(modeSelect).not.toBeUndefined();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
modeSelect!.value = "shared_workspace";
|
||||||
|
modeSelect!.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
});
|
||||||
|
await flush();
|
||||||
|
|
||||||
|
expect(container.textContent).toContain("will no longer use the parent issue workspace");
|
||||||
|
expect(container.textContent).toContain("Parent workspace");
|
||||||
|
|
||||||
|
act(() => root.unmount());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -301,6 +301,8 @@ export function NewIssueDialog() {
|
||||||
const isSubIssueMode = Boolean(newIssueDefaults.parentId);
|
const isSubIssueMode = Boolean(newIssueDefaults.parentId);
|
||||||
const parentIssueLabel = newIssueDefaults.parentIdentifier
|
const parentIssueLabel = newIssueDefaults.parentIdentifier
|
||||||
?? (newIssueDefaults.parentId ? newIssueDefaults.parentId.slice(0, 8) : "");
|
?? (newIssueDefaults.parentId ? newIssueDefaults.parentId.slice(0, 8) : "");
|
||||||
|
const parentExecutionWorkspaceId = newIssueDefaults.executionWorkspaceId ?? "";
|
||||||
|
const parentExecutionWorkspaceLabel = newIssueDefaults.parentExecutionWorkspaceLabel ?? parentExecutionWorkspaceId;
|
||||||
|
|
||||||
// Popover states
|
// Popover states
|
||||||
const [statusOpen, setStatusOpen] = useState(false);
|
const [statusOpen, setStatusOpen] = useState(false);
|
||||||
|
|
@ -517,18 +519,23 @@ export function NewIssueDialog() {
|
||||||
if (newIssueDefaults.parentId) {
|
if (newIssueDefaults.parentId) {
|
||||||
const defaultProjectId = newIssueDefaults.projectId ?? "";
|
const defaultProjectId = newIssueDefaults.projectId ?? "";
|
||||||
const defaultProject = orderedProjects.find((project) => project.id === defaultProjectId);
|
const defaultProject = orderedProjects.find((project) => project.id === defaultProjectId);
|
||||||
|
const defaultProjectWorkspaceId = newIssueDefaults.projectWorkspaceId
|
||||||
|
?? defaultProjectWorkspaceIdForProject(defaultProject);
|
||||||
|
const defaultExecutionWorkspaceMode = newIssueDefaults.executionWorkspaceId
|
||||||
|
? "reuse_existing"
|
||||||
|
: (newIssueDefaults.executionWorkspaceMode ?? defaultExecutionWorkspaceModeForProject(defaultProject));
|
||||||
setTitle(newIssueDefaults.title ?? "");
|
setTitle(newIssueDefaults.title ?? "");
|
||||||
setDescription(newIssueDefaults.description ?? "");
|
setDescription(newIssueDefaults.description ?? "");
|
||||||
setStatus(newIssueDefaults.status ?? "todo");
|
setStatus(newIssueDefaults.status ?? "todo");
|
||||||
setPriority(newIssueDefaults.priority ?? "");
|
setPriority(newIssueDefaults.priority ?? "");
|
||||||
setProjectId(defaultProjectId);
|
setProjectId(defaultProjectId);
|
||||||
setProjectWorkspaceId(defaultProjectWorkspaceIdForProject(defaultProject));
|
setProjectWorkspaceId(defaultProjectWorkspaceId);
|
||||||
setAssigneeValue(assigneeValueFromSelection(newIssueDefaults));
|
setAssigneeValue(assigneeValueFromSelection(newIssueDefaults));
|
||||||
setAssigneeModelOverride("");
|
setAssigneeModelOverride("");
|
||||||
setAssigneeThinkingEffort("");
|
setAssigneeThinkingEffort("");
|
||||||
setAssigneeChrome(false);
|
setAssigneeChrome(false);
|
||||||
setExecutionWorkspaceMode(defaultExecutionWorkspaceModeForProject(defaultProject));
|
setExecutionWorkspaceMode(defaultExecutionWorkspaceMode);
|
||||||
setSelectedExecutionWorkspaceId("");
|
setSelectedExecutionWorkspaceId(newIssueDefaults.executionWorkspaceId ?? "");
|
||||||
executionWorkspaceDefaultProjectId.current = defaultProjectId || null;
|
executionWorkspaceDefaultProjectId.current = defaultProjectId || null;
|
||||||
} else if (newIssueDefaults.title) {
|
} else if (newIssueDefaults.title) {
|
||||||
setTitle(newIssueDefaults.title);
|
setTitle(newIssueDefaults.title);
|
||||||
|
|
@ -797,6 +804,13 @@ export function NewIssueDialog() {
|
||||||
const selectedReusableExecutionWorkspace = deduplicatedReusableWorkspaces.find(
|
const selectedReusableExecutionWorkspace = deduplicatedReusableWorkspaces.find(
|
||||||
(workspace) => workspace.id === selectedExecutionWorkspaceId,
|
(workspace) => workspace.id === selectedExecutionWorkspaceId,
|
||||||
);
|
);
|
||||||
|
const isUsingParentExecutionWorkspace = isSubIssueMode && parentExecutionWorkspaceId
|
||||||
|
? executionWorkspaceMode === "reuse_existing" && selectedExecutionWorkspaceId === parentExecutionWorkspaceId
|
||||||
|
: false;
|
||||||
|
const showParentWorkspaceWarning = isSubIssueMode
|
||||||
|
&& currentProjectSupportsExecutionWorkspace
|
||||||
|
&& Boolean(parentExecutionWorkspaceId)
|
||||||
|
&& !isUsingParentExecutionWorkspace;
|
||||||
const assigneeOptionsTitle =
|
const assigneeOptionsTitle =
|
||||||
assigneeAdapterType === "claude_local"
|
assigneeAdapterType === "claude_local"
|
||||||
? "Claude options"
|
? "Claude options"
|
||||||
|
|
@ -1202,6 +1216,11 @@ export function NewIssueDialog() {
|
||||||
Reusing {selectedReusableExecutionWorkspace.name} from {selectedReusableExecutionWorkspace.branchName ?? selectedReusableExecutionWorkspace.cwd ?? "existing execution workspace"}.
|
Reusing {selectedReusableExecutionWorkspace.name} from {selectedReusableExecutionWorkspace.branchName ?? selectedReusableExecutionWorkspace.cwd ?? "existing execution workspace"}.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{showParentWorkspaceWarning ? (
|
||||||
|
<div className="rounded-md border border-amber-300/60 bg-amber-50 px-2 py-1.5 text-[11px] text-amber-900 dark:border-amber-800/70 dark:bg-amber-950/30 dark:text-amber-100">
|
||||||
|
Warning: this sub-issue will no longer use the parent issue workspace{parentExecutionWorkspaceLabel ? ` (${parentExecutionWorkspaceLabel})` : ""}.
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,14 @@ interface NewIssueDefaults {
|
||||||
status?: string;
|
status?: string;
|
||||||
priority?: string;
|
priority?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
projectWorkspaceId?: string;
|
||||||
goalId?: string;
|
goalId?: string;
|
||||||
parentId?: string;
|
parentId?: string;
|
||||||
parentIdentifier?: string;
|
parentIdentifier?: string;
|
||||||
parentTitle?: string;
|
parentTitle?: string;
|
||||||
|
executionWorkspaceId?: string;
|
||||||
|
executionWorkspaceMode?: string;
|
||||||
|
parentExecutionWorkspaceLabel?: string;
|
||||||
assigneeAgentId?: string;
|
assigneeAgentId?: string;
|
||||||
assigneeUserId?: string;
|
assigneeUserId?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
|
||||||
|
|
@ -507,9 +507,31 @@ export function IssueDetail() {
|
||||||
parentIdentifier: issue.identifier ?? undefined,
|
parentIdentifier: issue.identifier ?? undefined,
|
||||||
parentTitle: issue.title,
|
parentTitle: issue.title,
|
||||||
projectId: issue.projectId ?? undefined,
|
projectId: issue.projectId ?? undefined,
|
||||||
|
projectWorkspaceId: issue.projectWorkspaceId ?? undefined,
|
||||||
goalId: issue.goalId ?? 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,
|
||||||
});
|
});
|
||||||
}, [issue?.goalId, issue?.id, issue?.identifier, issue?.projectId, issue?.title, openNewIssue]);
|
}, [
|
||||||
|
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,
|
||||||
|
openNewIssue,
|
||||||
|
]);
|
||||||
|
|
||||||
const commentReassignOptions = useMemo(() => {
|
const commentReassignOptions = useMemo(() => {
|
||||||
const options: Array<{ id: string; label: string; searchText?: string }> = [];
|
const options: Array<{ id: string; label: string; searchText?: string }> = [];
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue