mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +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 () => {
|
||||
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 = {
|
||||
parentId: "issue-1",
|
||||
parentIdentifier: "PAP-1",
|
||||
parentTitle: "Parent issue",
|
||||
title: "Child issue",
|
||||
projectId: "project-1",
|
||||
executionWorkspaceId: "workspace-1",
|
||||
goalId: "goal-1",
|
||||
};
|
||||
|
||||
|
|
@ -340,9 +365,75 @@ describe("NewIssueDialog", () => {
|
|||
parentId: "issue-1",
|
||||
goalId: "goal-1",
|
||||
projectId: "project-1",
|
||||
executionWorkspaceId: "workspace-1",
|
||||
}),
|
||||
);
|
||||
|
||||
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 parentIssueLabel = newIssueDefaults.parentIdentifier
|
||||
?? (newIssueDefaults.parentId ? newIssueDefaults.parentId.slice(0, 8) : "");
|
||||
const parentExecutionWorkspaceId = newIssueDefaults.executionWorkspaceId ?? "";
|
||||
const parentExecutionWorkspaceLabel = newIssueDefaults.parentExecutionWorkspaceLabel ?? parentExecutionWorkspaceId;
|
||||
|
||||
// Popover states
|
||||
const [statusOpen, setStatusOpen] = useState(false);
|
||||
|
|
@ -517,18 +519,23 @@ export function NewIssueDialog() {
|
|||
if (newIssueDefaults.parentId) {
|
||||
const defaultProjectId = newIssueDefaults.projectId ?? "";
|
||||
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 ?? "");
|
||||
setDescription(newIssueDefaults.description ?? "");
|
||||
setStatus(newIssueDefaults.status ?? "todo");
|
||||
setPriority(newIssueDefaults.priority ?? "");
|
||||
setProjectId(defaultProjectId);
|
||||
setProjectWorkspaceId(defaultProjectWorkspaceIdForProject(defaultProject));
|
||||
setProjectWorkspaceId(defaultProjectWorkspaceId);
|
||||
setAssigneeValue(assigneeValueFromSelection(newIssueDefaults));
|
||||
setAssigneeModelOverride("");
|
||||
setAssigneeThinkingEffort("");
|
||||
setAssigneeChrome(false);
|
||||
setExecutionWorkspaceMode(defaultExecutionWorkspaceModeForProject(defaultProject));
|
||||
setSelectedExecutionWorkspaceId("");
|
||||
setExecutionWorkspaceMode(defaultExecutionWorkspaceMode);
|
||||
setSelectedExecutionWorkspaceId(newIssueDefaults.executionWorkspaceId ?? "");
|
||||
executionWorkspaceDefaultProjectId.current = defaultProjectId || null;
|
||||
} else if (newIssueDefaults.title) {
|
||||
setTitle(newIssueDefaults.title);
|
||||
|
|
@ -797,6 +804,13 @@ export function NewIssueDialog() {
|
|||
const selectedReusableExecutionWorkspace = deduplicatedReusableWorkspaces.find(
|
||||
(workspace) => workspace.id === selectedExecutionWorkspaceId,
|
||||
);
|
||||
const isUsingParentExecutionWorkspace = isSubIssueMode && parentExecutionWorkspaceId
|
||||
? executionWorkspaceMode === "reuse_existing" && selectedExecutionWorkspaceId === parentExecutionWorkspaceId
|
||||
: false;
|
||||
const showParentWorkspaceWarning = isSubIssueMode
|
||||
&& currentProjectSupportsExecutionWorkspace
|
||||
&& Boolean(parentExecutionWorkspaceId)
|
||||
&& !isUsingParentExecutionWorkspace;
|
||||
const assigneeOptionsTitle =
|
||||
assigneeAdapterType === "claude_local"
|
||||
? "Claude options"
|
||||
|
|
@ -1202,6 +1216,11 @@ export function NewIssueDialog() {
|
|||
Reusing {selectedReusableExecutionWorkspace.name} from {selectedReusableExecutionWorkspace.branchName ?? selectedReusableExecutionWorkspace.cwd ?? "existing execution workspace"}.
|
||||
</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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,14 @@ interface NewIssueDefaults {
|
|||
status?: string;
|
||||
priority?: string;
|
||||
projectId?: string;
|
||||
projectWorkspaceId?: string;
|
||||
goalId?: string;
|
||||
parentId?: string;
|
||||
parentIdentifier?: string;
|
||||
parentTitle?: string;
|
||||
executionWorkspaceId?: string;
|
||||
executionWorkspaceMode?: string;
|
||||
parentExecutionWorkspaceLabel?: string;
|
||||
assigneeAgentId?: string;
|
||||
assigneeUserId?: string;
|
||||
title?: string;
|
||||
|
|
|
|||
|
|
@ -507,9 +507,31 @@ export function IssueDetail() {
|
|||
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,
|
||||
});
|
||||
}, [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 options: Array<{ id: string; label: string; searchText?: string }> = [];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue