Skip to content

Commit f74b447

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

2 files changed

Lines changed: 112 additions & 36 deletions

File tree

apps/web/src/components/Sidebar.tsx

Lines changed: 75 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -752,9 +752,7 @@ export default function Sidebar() {
752752
"This permanently clears conversation history for this thread.",
753753
].join("\n"),
754754
);
755-
if (!confirmed) {
756-
return;
757-
}
755+
if (!confirmed) return;
758756
}
759757
await deleteThread(threadId);
760758
},
@@ -821,6 +819,37 @@ export default function Sidebar() {
821819
],
822820
);
823821

822+
const removeProject = useCallback(
823+
async (projectId: ProjectId): Promise<void> => {
824+
const api = readNativeApi();
825+
if (!api) return;
826+
const project = projects.find((entry) => entry.id === projectId);
827+
if (!project) return;
828+
829+
try {
830+
const projectDraftThread = getDraftThreadByProjectId(projectId);
831+
if (projectDraftThread) {
832+
clearComposerDraftForThread(projectDraftThread.threadId);
833+
}
834+
clearProjectDraftThreadId(projectId);
835+
await api.orchestration.dispatchCommand({
836+
type: "project.delete",
837+
commandId: newCommandId(),
838+
projectId,
839+
});
840+
} catch (error) {
841+
const message = error instanceof Error ? error.message : "Unknown error removing project.";
842+
console.error("Failed to remove project", { projectId, error });
843+
toastManager.add({
844+
type: "error",
845+
title: `Failed to remove "${project.name}"`,
846+
description: message,
847+
});
848+
}
849+
},
850+
[clearComposerDraftForThread, clearProjectDraftThreadId, getDraftThreadByProjectId, projects],
851+
);
852+
824853
const handleThreadClick = useCallback(
825854
(event: MouseEvent, threadId: ThreadId, orderedProjectThreadIds: readonly ThreadId[]) => {
826855
const isMac = isMacPlatform(navigator.platform);
@@ -874,45 +903,59 @@ export default function Sidebar() {
874903

875904
const projectThreads = threads.filter((thread) => thread.projectId === projectId);
876905
if (projectThreads.length > 0) {
877-
toastManager.add({
906+
const warningToastId = toastManager.add({
878907
type: "warning",
879908
title: "Project is not empty",
880909
description: "Delete all threads in this project before removing it.",
910+
data: {
911+
actionLayout: "stacked-end",
912+
actionVariant: "destructive",
913+
},
914+
actionProps: {
915+
children: "Delete anyway",
916+
onClick: () => {
917+
void (async () => {
918+
toastManager.close(warningToastId);
919+
await new Promise<void>((resolve) => {
920+
window.setTimeout(resolve, 180);
921+
});
922+
const confirmed = await api.dialogs.confirm(
923+
[
924+
`Remove project "${project.name}" and delete its ${projectThreads.length} thread${
925+
projectThreads.length === 1 ? "" : "s"
926+
}?`,
927+
"This will permanently clear conversation history for those threads.",
928+
"This action cannot be undone.",
929+
].join("\n"),
930+
);
931+
if (!confirmed) return;
932+
933+
const deletedThreadIds = new Set<ThreadId>(
934+
projectThreads.map((thread) => thread.id),
935+
);
936+
for (const thread of projectThreads) {
937+
await deleteThread(thread.id, { deletedThreadIds });
938+
}
939+
await removeProject(projectId);
940+
})().catch((error) => {
941+
toastManager.add({
942+
type: "error",
943+
title: `Failed to remove "${project.name}"`,
944+
description:
945+
error instanceof Error ? error.message : "Unknown error removing project.",
946+
});
947+
});
948+
},
949+
},
881950
});
882951
return;
883952
}
884953

885954
const confirmed = await api.dialogs.confirm(`Remove project "${project.name}"?`);
886955
if (!confirmed) return;
887-
888-
try {
889-
const projectDraftThread = getDraftThreadByProjectId(projectId);
890-
if (projectDraftThread) {
891-
clearComposerDraftForThread(projectDraftThread.threadId);
892-
}
893-
clearProjectDraftThreadId(projectId);
894-
await api.orchestration.dispatchCommand({
895-
type: "project.delete",
896-
commandId: newCommandId(),
897-
projectId,
898-
});
899-
} catch (error) {
900-
const message = error instanceof Error ? error.message : "Unknown error removing project.";
901-
console.error("Failed to remove project", { projectId, error });
902-
toastManager.add({
903-
type: "error",
904-
title: `Failed to remove "${project.name}"`,
905-
description: message,
906-
});
907-
}
956+
await removeProject(projectId);
908957
},
909-
[
910-
clearComposerDraftForThread,
911-
clearProjectDraftThreadId,
912-
getDraftThreadByProjectId,
913-
projects,
914-
threads,
915-
],
958+
[deleteThread, projects, removeProject, threads],
916959
);
917960

918961
const projectDnDSensors = useSensors(

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

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ type ThreadToastData = {
2020
threadId?: ThreadId | null;
2121
tooltipStyle?: boolean;
2222
dismissAfterVisibleMs?: number;
23+
actionLayout?: "inline" | "stacked-end";
24+
actionVariant?:
25+
| "default"
26+
| "destructive"
27+
| "destructive-outline"
28+
| "ghost"
29+
| "link"
30+
| "outline"
31+
| "secondary";
2332
};
2433

2534
const toastManager = Toast.createToastManager<ThreadToastData>();
@@ -196,6 +205,9 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {
196205
visibleIndex,
197206
visibleToastLayout.items.length,
198207
);
208+
const stackedActionLayout =
209+
toast.actionProps !== undefined && toast.data?.actionLayout === "stacked-end";
210+
const actionVariant = toast.data?.actionVariant ?? "default";
199211

200212
return (
201213
<Toast.Root
@@ -268,7 +280,10 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {
268280
/>
269281
<Toast.Content
270282
className={cn(
271-
"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",
283+
"pointer-events-auto overflow-hidden px-3.5 text-sm transition-opacity duration-250 data-expanded:opacity-100",
284+
stackedActionLayout
285+
? "flex flex-col gap-2 py-2.5"
286+
: "flex items-center justify-between gap-1.5 py-3",
272287
hideCollapsedContent &&
273288
"not-data-expanded:pointer-events-none not-data-expanded:opacity-0",
274289
)}
@@ -296,7 +311,11 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {
296311
</div>
297312
{toast.actionProps && (
298313
<Toast.Action
299-
className={cn(buttonVariants({ size: "xs" }), "shrink-0")}
314+
className={cn(
315+
buttonVariants({ size: "xs", variant: actionVariant }),
316+
"shrink-0",
317+
stackedActionLayout && "self-end",
318+
)}
300319
data-slot="toast-action"
301320
>
302321
{toast.actionProps.children}
@@ -333,6 +352,9 @@ function AnchoredToasts() {
333352
const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null;
334353
const tooltipStyle = toast.data?.tooltipStyle ?? false;
335354
const positionerProps = toast.positionerProps;
355+
const stackedActionLayout =
356+
toast.actionProps !== undefined && toast.data?.actionLayout === "stacked-end";
357+
const actionVariant = toast.data?.actionVariant ?? "default";
336358

337359
if (!positionerProps?.anchor) {
338360
return null;
@@ -361,7 +383,14 @@ function AnchoredToasts() {
361383
<Toast.Title data-slot="toast-title" />
362384
</Toast.Content>
363385
) : (
364-
<Toast.Content className="pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm">
386+
<Toast.Content
387+
className={cn(
388+
"pointer-events-auto overflow-hidden px-3.5 text-sm",
389+
stackedActionLayout
390+
? "flex flex-col gap-2 py-2.5"
391+
: "flex items-center justify-between gap-1.5 py-3",
392+
)}
393+
>
365394
<div className="flex min-w-0 flex-1 gap-2">
366395
{Icon && (
367396
<div
@@ -385,7 +414,11 @@ function AnchoredToasts() {
385414
</div>
386415
{toast.actionProps && (
387416
<Toast.Action
388-
className={cn(buttonVariants({ size: "xs" }), "shrink-0")}
417+
className={cn(
418+
buttonVariants({ size: "xs", variant: actionVariant }),
419+
"shrink-0",
420+
stackedActionLayout && "self-end",
421+
)}
389422
data-slot="toast-action"
390423
>
391424
{toast.actionProps.children}

0 commit comments

Comments
 (0)