Skip to content

Commit b3f029d

Browse files
committed
fix(web): allow deleting non-empty projects from the warning toast
1 parent c6f57a1 commit b3f029d

2 files changed

Lines changed: 110 additions & 37 deletions

File tree

apps/web/src/components/Sidebar.tsx

Lines changed: 73 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,9 +1074,7 @@ export default function Sidebar() {
10741074
"This permanently clears conversation history for this thread.",
10751075
].join("\n"),
10761076
);
1077-
if (!confirmed) {
1078-
return;
1079-
}
1077+
if (!confirmed) return;
10801078
}
10811079
await deleteThread(threadId);
10821080
},
@@ -1145,6 +1143,37 @@ export default function Sidebar() {
11451143
],
11461144
);
11471145

1146+
const removeProject = useCallback(
1147+
async (projectId: ProjectId): Promise<void> => {
1148+
const api = readNativeApi();
1149+
if (!api) return;
1150+
const project = projects.find((entry) => entry.id === projectId);
1151+
if (!project) return;
1152+
1153+
try {
1154+
const projectDraftThread = getDraftThreadByProjectId(projectId);
1155+
if (projectDraftThread) {
1156+
clearComposerDraftForThread(projectDraftThread.threadId);
1157+
}
1158+
clearProjectDraftThreadId(projectId);
1159+
await api.orchestration.dispatchCommand({
1160+
type: "project.delete",
1161+
commandId: newCommandId(),
1162+
projectId,
1163+
});
1164+
} catch (error) {
1165+
const message = error instanceof Error ? error.message : "Unknown error removing project.";
1166+
console.error("Failed to remove project", { projectId, error });
1167+
toastManager.add({
1168+
type: "error",
1169+
title: `Failed to remove "${project.name}"`,
1170+
description: message,
1171+
});
1172+
}
1173+
},
1174+
[clearComposerDraftForThread, clearProjectDraftThreadId, getDraftThreadByProjectId, projects],
1175+
);
1176+
11481177
const handleThreadClick = useCallback(
11491178
(event: MouseEvent, threadId: ThreadId, orderedProjectThreadIds: readonly ThreadId[]) => {
11501179
const isMac = isMacPlatform(navigator.platform);
@@ -1219,46 +1248,57 @@ export default function Sidebar() {
12191248

12201249
const projectThreadIds = threadIdsByProjectId[projectId] ?? [];
12211250
if (projectThreadIds.length > 0) {
1222-
toastManager.add({
1251+
const warningToastId = toastManager.add({
12231252
type: "warning",
12241253
title: "Project is not empty",
12251254
description: "Delete all threads in this project before removing it.",
1255+
data: {
1256+
actionLayout: "stacked-end",
1257+
actionVariant: "destructive",
1258+
},
1259+
actionProps: {
1260+
children: "Delete anyway",
1261+
onClick: () => {
1262+
void (async () => {
1263+
toastManager.close(warningToastId);
1264+
await new Promise<void>((resolve) => {
1265+
window.setTimeout(resolve, 180);
1266+
});
1267+
const confirmed = await api.dialogs.confirm(
1268+
[
1269+
`Remove project "${project.name}" and delete its ${projectThreadIds.length} thread${
1270+
projectThreadIds.length === 1 ? "" : "s"
1271+
}?`,
1272+
"This will permanently clear conversation history for those threads.",
1273+
"This action cannot be undone.",
1274+
].join("\n"),
1275+
);
1276+
if (!confirmed) return;
1277+
1278+
const deletedThreadIds = new Set<ThreadId>(projectThreadIds);
1279+
for (const threadId of projectThreadIds) {
1280+
await deleteThread(threadId, { deletedThreadIds });
1281+
}
1282+
await removeProject(projectId);
1283+
})().catch((error) => {
1284+
toastManager.add({
1285+
type: "error",
1286+
title: `Failed to remove "${project.name}"`,
1287+
description:
1288+
error instanceof Error ? error.message : "Unknown error removing project.",
1289+
});
1290+
});
1291+
},
1292+
},
12261293
});
12271294
return;
12281295
}
12291296

12301297
const confirmed = await api.dialogs.confirm(`Remove project "${project.name}"?`);
12311298
if (!confirmed) return;
1232-
1233-
try {
1234-
const projectDraftThread = getDraftThreadByProjectId(projectId);
1235-
if (projectDraftThread) {
1236-
clearComposerDraftForThread(projectDraftThread.threadId);
1237-
}
1238-
clearProjectDraftThreadId(projectId);
1239-
await api.orchestration.dispatchCommand({
1240-
type: "project.delete",
1241-
commandId: newCommandId(),
1242-
projectId,
1243-
});
1244-
} catch (error) {
1245-
const message = error instanceof Error ? error.message : "Unknown error removing project.";
1246-
console.error("Failed to remove project", { projectId, error });
1247-
toastManager.add({
1248-
type: "error",
1249-
title: `Failed to remove "${project.name}"`,
1250-
description: message,
1251-
});
1252-
}
1299+
await removeProject(projectId);
12531300
},
1254-
[
1255-
clearComposerDraftForThread,
1256-
clearProjectDraftThreadId,
1257-
copyPathToClipboard,
1258-
getDraftThreadByProjectId,
1259-
projects,
1260-
threadIdsByProjectId,
1261-
],
1301+
[copyPathToClipboard, deleteThread, projects, removeProject, threadIdsByProjectId],
12621302
);
12631303

12641304
const projectDnDSensors = useSensors(

apps/web/src/components/ui/toast.tsx

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ export type ThreadToastData = {
2424
tooltipStyle?: boolean;
2525
dismissAfterVisibleMs?: number;
2626
hideCopyButton?: boolean;
27+
actionLayout?: "inline" | "stacked-end";
28+
actionVariant?:
29+
| "default"
30+
| "destructive"
31+
| "destructive-outline"
32+
| "ghost"
33+
| "link"
34+
| "outline"
35+
| "secondary";
2736
};
2837

2938
const toastManager = Toast.createToastManager<ThreadToastData>();
@@ -219,6 +228,9 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {
219228
visibleIndex,
220229
visibleToastLayout.items.length,
221230
);
231+
const stackedActionLayout =
232+
toast.actionProps !== undefined && toast.data?.actionLayout === "stacked-end";
233+
const actionVariant = toast.data?.actionVariant ?? "default";
222234

223235
return (
224236
<Toast.Root
@@ -291,7 +303,10 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {
291303
/>
292304
<Toast.Content
293305
className={cn(
294-
"pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm transition-opacity duration-250 data-expanded:opacity-100",
306+
"pointer-events-auto overflow-hidden px-3.5 text-sm transition-opacity duration-250 data-expanded:opacity-100",
307+
stackedActionLayout
308+
? "flex flex-col gap-2 py-2.5"
309+
: "flex items-center justify-between gap-1.5 py-3",
295310
hideCollapsedContent &&
296311
"not-data-expanded:pointer-events-none not-data-expanded:opacity-0",
297312
)}
@@ -324,7 +339,11 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {
324339
</div>
325340
{toast.actionProps && (
326341
<Toast.Action
327-
className={cn(buttonVariants({ size: "xs" }), "shrink-0")}
342+
className={cn(
343+
buttonVariants({ size: "xs", variant: actionVariant }),
344+
"shrink-0",
345+
stackedActionLayout && "self-end",
346+
)}
328347
data-slot="toast-action"
329348
>
330349
{toast.actionProps.children}
@@ -361,6 +380,9 @@ function AnchoredToasts() {
361380
const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null;
362381
const tooltipStyle = toast.data?.tooltipStyle ?? false;
363382
const positionerProps = toast.positionerProps;
383+
const stackedActionLayout =
384+
toast.actionProps !== undefined && toast.data?.actionLayout === "stacked-end";
385+
const actionVariant = toast.data?.actionVariant ?? "default";
364386

365387
if (!positionerProps?.anchor) {
366388
return null;
@@ -389,7 +411,14 @@ function AnchoredToasts() {
389411
<Toast.Title data-slot="toast-title" />
390412
</Toast.Content>
391413
) : (
392-
<Toast.Content className="pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm">
414+
<Toast.Content
415+
className={cn(
416+
"pointer-events-auto overflow-hidden px-3.5 text-sm",
417+
stackedActionLayout
418+
? "flex flex-col gap-2 py-2.5"
419+
: "flex items-center justify-between gap-1.5 py-3",
420+
)}
421+
>
393422
<div className="flex min-w-0 flex-1 gap-2">
394423
{Icon && (
395424
<div
@@ -420,7 +449,11 @@ function AnchoredToasts() {
420449
</div>
421450
{toast.actionProps && (
422451
<Toast.Action
423-
className={cn(buttonVariants({ size: "xs" }), "shrink-0")}
452+
className={cn(
453+
buttonVariants({ size: "xs", variant: actionVariant }),
454+
"shrink-0",
455+
stackedActionLayout && "self-end",
456+
)}
424457
data-slot="toast-action"
425458
>
426459
{toast.actionProps.children}

0 commit comments

Comments
 (0)