Skip to content

Commit 0d66120

Browse files
committed
🐛 fix(desktop): stage collection dialog changes
1 parent 35ce555 commit 0d66120

2 files changed

Lines changed: 107 additions & 54 deletions

File tree

Lines changed: 101 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { useEffect, useRef, useState } from "react";
12
import type { PromptDetail } from "../../../shared/ipc.js";
23
import { useAppMutation, useCollections } from "../hooks/use-data";
4+
import { DialogShell } from "./dialogs";
35

46
export function MoveToCollectionDialog({
57
prompt,
@@ -11,65 +13,111 @@ export function MoveToCollectionDialog({
1113
onOpenChange: (open: boolean) => void;
1214
}) {
1315
const { data: collections } = useCollections();
14-
const toggle = useAppMutation(
15-
async ({ collectionId, member }: { collectionId: string; member: boolean }) => {
16-
if (member) await window.promptBuilder.collections.removePrompt(collectionId, prompt.id);
17-
else await window.promptBuilder.collections.addPrompt(collectionId, prompt.id);
16+
const [selectedCollectionIds, setSelectedCollectionIds] = useState<string[]>(prompt.collectionIds);
17+
const wasOpen = useRef(false);
18+
const previousPromptId = useRef(prompt.id);
19+
const updateMemberships = useAppMutation(
20+
async ({ add, remove }: { add: string[]; remove: string[] }) => {
21+
await Promise.all([
22+
...add.map((collectionId) =>
23+
window.promptBuilder.collections.addPrompt(collectionId, prompt.id),
24+
),
25+
...remove.map((collectionId) =>
26+
window.promptBuilder.collections.removePrompt(collectionId, prompt.id),
27+
),
28+
]);
1829
},
1930
{ quiet: true },
2031
);
2132

22-
if (!open) return null;
33+
useEffect(() => {
34+
if (open && (!wasOpen.current || previousPromptId.current !== prompt.id)) {
35+
setSelectedCollectionIds(prompt.collectionIds);
36+
}
37+
wasOpen.current = open;
38+
previousPromptId.current = prompt.id;
39+
}, [open, prompt.id, prompt.collectionIds]);
40+
41+
const save = async () => {
42+
const current = new Set(prompt.collectionIds);
43+
const selected = new Set(selectedCollectionIds);
44+
const add = selectedCollectionIds.filter((id) => !current.has(id));
45+
const remove = prompt.collectionIds.filter((id) => !selected.has(id));
46+
47+
if (add.length === 0 && remove.length === 0) {
48+
onOpenChange(false);
49+
return;
50+
}
51+
52+
try {
53+
await updateMemberships.mutateAsync({ add, remove });
54+
onOpenChange(false);
55+
} catch {
56+
// Keep the draft open so the user can retry after the mutation error toast.
57+
}
58+
};
59+
2360
return (
24-
<div
25-
className="pb-overlay fixed inset-0 z-40 flex items-center justify-center bg-black/60"
26-
onClick={() => onOpenChange(false)}
61+
<DialogShell
62+
open={open}
63+
onOpenChange={onOpenChange}
64+
title="Move to collection"
65+
width="max-w-sm"
2766
>
28-
<div
29-
className="max-h-[calc(100vh-2rem)] w-[calc(100vw-2rem)] max-w-sm overflow-y-auto rounded-xl border border-line-strong bg-panel p-5 shadow-2xl"
30-
onClick={(event) => event.stopPropagation()}
31-
role="dialog"
32-
aria-label="Move to collection"
33-
>
34-
<h2 className="mb-3 text-sm font-semibold text-ink">Collections</h2>
35-
<div className="max-h-64 space-y-0.5 overflow-y-auto">
36-
{(collections ?? []).length === 0 && (
37-
<p className="text-[12px] text-ink-faint">
38-
No collections yet — create one from the left rail.
39-
</p>
40-
)}
41-
{(collections ?? []).map((collection) => {
42-
const member = prompt.collectionIds.includes(collection.id);
43-
return (
44-
<label
45-
key={collection.id}
46-
className="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-[13px] text-ink-dim hover:bg-hover"
47-
>
48-
<input
49-
type="checkbox"
50-
aria-label={collection.name}
51-
checked={member}
52-
onChange={() => toggle.mutate({ collectionId: collection.id, member })}
53-
className="accent-accent"
54-
/>
55-
{collection.name}
56-
<span className="ml-auto text-[11px] tabular-nums text-ink-faint">
57-
{collection.promptCount}
58-
</span>
59-
</label>
60-
);
61-
})}
62-
</div>
63-
<div className="mt-4 flex justify-end">
64-
<button
65-
type="button"
66-
onClick={() => onOpenChange(false)}
67-
className="rounded-md border border-line px-3 py-1.5 text-[13px] text-ink-dim transition-colors hover:bg-hover hover:text-ink"
68-
>
69-
Done
70-
</button>
71-
</div>
67+
<div className="max-h-64 space-y-0.5 overflow-y-auto">
68+
{(collections ?? []).length === 0 && (
69+
<p className="text-[12px] text-ink-faint">
70+
No collections yet — create one from the left rail.
71+
</p>
72+
)}
73+
{(collections ?? []).map((collection) => {
74+
const member = selectedCollectionIds.includes(collection.id);
75+
return (
76+
<label
77+
key={collection.id}
78+
className="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-[13px] text-ink-dim hover:bg-hover"
79+
>
80+
<input
81+
type="checkbox"
82+
aria-label={collection.name}
83+
checked={member}
84+
disabled={updateMemberships.isPending}
85+
onChange={(event) => {
86+
const checked = event.currentTarget.checked;
87+
setSelectedCollectionIds((current) => {
88+
if (checked) {
89+
return current.includes(collection.id) ? current : [...current, collection.id];
90+
}
91+
return current.filter((id) => id !== collection.id);
92+
});
93+
}}
94+
className="accent-accent"
95+
/>
96+
{collection.name}
97+
<span className="ml-auto text-[11px] tabular-nums text-ink-faint">
98+
{collection.promptCount}
99+
</span>
100+
</label>
101+
);
102+
})}
103+
</div>
104+
<div className="mt-4 flex justify-end gap-2">
105+
<button
106+
type="button"
107+
onClick={() => onOpenChange(false)}
108+
className="rounded-md border border-line px-3 py-1.5 text-[13px] text-ink-dim transition-colors hover:bg-hover hover:text-ink"
109+
>
110+
Cancel
111+
</button>
112+
<button
113+
type="button"
114+
onClick={() => void save()}
115+
disabled={updateMemberships.isPending}
116+
className="rounded-md bg-accent px-3 py-1.5 text-[13px] font-medium text-white transition-colors hover:bg-accent-strong disabled:cursor-not-allowed disabled:opacity-40"
117+
>
118+
Save
119+
</button>
72120
</div>
73-
</div>
121+
</DialogShell>
74122
);
75123
}

‎apps/desktop/src/renderer/src/components/prompt-list-pane.test.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,12 @@ describe("PromptListPane context menu", () => {
186186
const menu = await openPromptMenu(user);
187187
await user.click(menu.getByRole("menuitem", { name: "Move to collection…" }));
188188
const dialog = await screen.findByRole("dialog", { name: "Move to collection" });
189-
await user.click(within(dialog).getByRole("checkbox", { name: "Work" }));
189+
const work = within(dialog).getByRole("checkbox", { name: "Work" });
190+
await user.click(work);
191+
expect(work).toBeChecked();
192+
expect(bridge.collections.addPrompt).not.toHaveBeenCalled();
193+
194+
await user.click(within(dialog).getByRole("button", { name: "Save" }));
190195

191196
await waitFor(() =>
192197
expect(bridge.collections.addPrompt).toHaveBeenCalledWith("collection-1", beta.id),

0 commit comments

Comments
 (0)