Skip to content

Commit 52a831b

Browse files
committed
🤖 fix: disable project removal when workspaces exist
- Remove project button is now disabled when project has any workspaces - Shows explanatory tooltip: 'Delete workspace first' (1) or 'Delete all N workspaces first' (N) - Button uses aria-disabled and cursor-not-allowed styling when disabled - Updated storybook test to verify disabled state instead of backend error
1 parent 534ee43 commit 52a831b

File tree

2 files changed

+55
-47
lines changed

2 files changed

+55
-47
lines changed

src/browser/components/ProjectSidebar.tsx

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -559,34 +559,51 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
559559
</TooltipTrigger>
560560
<TooltipContent align="end">Manage secrets</TooltipContent>
561561
</Tooltip>
562-
<Tooltip>
563-
<TooltipTrigger asChild>
564-
<button
565-
onClick={(event) => {
566-
event.stopPropagation();
567-
const buttonElement = event.currentTarget;
568-
void (async () => {
569-
const result = await onRemoveProject(projectPath);
570-
if (!result.success) {
571-
const error = result.error ?? "Failed to remove project";
572-
const rect = buttonElement.getBoundingClientRect();
573-
const anchor = {
574-
top: rect.top + window.scrollY,
575-
left: rect.right + 10,
576-
};
577-
projectRemoveError.showError(projectPath, error, anchor);
578-
}
579-
})();
580-
}}
581-
aria-label={`Remove project ${projectName}`}
582-
data-project-path={projectPath}
583-
className="text-muted-dark hover:text-danger-light hover:bg-danger-light/10 mr-1 flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded-[3px] border-none bg-transparent text-base opacity-0 transition-all duration-200"
584-
>
585-
×
586-
</button>
587-
</TooltipTrigger>
588-
<TooltipContent align="end">Remove project</TooltipContent>
589-
</Tooltip>
562+
{(() => {
563+
const workspaceCount =
564+
sortedWorkspacesByProject.get(projectPath)?.length ?? 0;
565+
const canDelete = workspaceCount === 0;
566+
return (
567+
<Tooltip>
568+
<TooltipTrigger asChild>
569+
<button
570+
onClick={(event) => {
571+
event.stopPropagation();
572+
if (!canDelete) return;
573+
const buttonElement = event.currentTarget;
574+
void (async () => {
575+
const result = await onRemoveProject(projectPath);
576+
if (!result.success) {
577+
const error = result.error ?? "Failed to remove project";
578+
const rect = buttonElement.getBoundingClientRect();
579+
const anchor = {
580+
top: rect.top + window.scrollY,
581+
left: rect.right + 10,
582+
};
583+
projectRemoveError.showError(projectPath, error, anchor);
584+
}
585+
})();
586+
}}
587+
aria-label={`Remove project ${projectName}`}
588+
aria-disabled={!canDelete}
589+
data-project-path={projectPath}
590+
className={`mr-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-[3px] border-none bg-transparent text-base opacity-0 transition-all duration-200 ${
591+
canDelete
592+
? "text-muted-dark hover:text-danger-light hover:bg-danger-light/10 cursor-pointer"
593+
: "text-muted cursor-not-allowed"
594+
}`}
595+
>
596+
×
597+
</button>
598+
</TooltipTrigger>
599+
<TooltipContent align="end">
600+
{canDelete
601+
? "Remove project"
602+
: `Delete ${workspaceCount === 1 ? "workspace" : `all ${workspaceCount} workspaces`} first`}
603+
</TooltipContent>
604+
</Tooltip>
605+
);
606+
})()}
590607
<button
591608
onClick={(event) => {
592609
event.stopPropagation();

src/browser/stories/App.errors.stories.tsx

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -248,12 +248,12 @@ export const LargeDiff: AppStory = {
248248
};
249249

250250
/**
251-
* Project removal error popover.
251+
* Project removal disabled when multiple workspaces exist.
252252
*
253-
* Shows the error popup when attempting to remove a project that has active workspaces.
254-
* The play function hovers the project and clicks the remove button to trigger the error.
253+
* The remove button is disabled and shows an explanatory tooltip when a project
254+
* has more than one workspace. The play function hovers to reveal the disabled state.
255255
*/
256-
export const ProjectRemovalError: AppStory = {
256+
export const ProjectRemovalDisabled: AppStory = {
257257
render: () => (
258258
<AppWithMocks
259259
setup={() => {
@@ -268,11 +268,6 @@ export const ProjectRemovalError: AppStory = {
268268
return createMockORPCClient({
269269
projects: groupWorkspacesByProject(workspaces),
270270
workspaces,
271-
onProjectRemove: () => ({
272-
success: false,
273-
error:
274-
"Cannot remove project with active workspaces. Please remove all 2 workspace(s) first.",
275-
}),
276271
});
277272
}}
278273
/>
@@ -297,16 +292,12 @@ export const ProjectRemovalError: AppStory = {
297292
// Small delay for hover state to apply
298293
await new Promise((r) => setTimeout(r, 100));
299294

300-
// Click the remove button
301-
await userEvent.click(removeButton);
295+
// Verify the button is disabled
296+
if (removeButton.getAttribute("aria-disabled") !== "true") {
297+
throw new Error("Remove button should be disabled when project has multiple workspaces");
298+
}
302299

303-
// Wait for the error popover to appear
304-
await waitFor(
305-
() => {
306-
const errorPopover = document.querySelector('[role="alert"]');
307-
if (!errorPopover) throw new Error("Error popover not found");
308-
},
309-
{ timeout: 2000 }
310-
);
300+
// Hover the disabled button to trigger tooltip
301+
await userEvent.hover(removeButton);
311302
},
312303
};

0 commit comments

Comments
 (0)