1+ import { useEffect , useRef , useState } from "react" ;
12import type { PromptDetail } from "../../../shared/ipc.js" ;
23import { useAppMutation , useCollections } from "../hooks/use-data" ;
4+ import { DialogShell } from "./dialogs" ;
35
46export 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}
0 commit comments