11'use client' ;
22
33/**
4- * Destination picker for "Move". A small folder browser: navigate the space's tree, then "Move
5- * here". Destinations stay INSIDE the space (root = the space folder itself), so a move can never
6- * strand a file outside every space. The moved folder and its subtree are excluded to prevent
7- * cycles.
4+ * Destination picker for "Move". A small folder browser across ALL your compatible spaces: first
5+ * pick a destination space, then navigate its tree and "Move here". Staying inside real spaces (a
6+ * space's root is the space folder itself) means a move can never strand a file outside every space.
7+ * The moved folder and its subtree are excluded to prevent cycles.
88 */
99import { useMemo , useState } from 'react' ;
1010import { createPortal } from 'react-dom' ;
11- import { Folder , ChevronRight , Home , X , FolderInput , Check } from 'lucide-react' ;
11+ import { Folder , ChevronRight , Home , X , FolderInput , Check , FolderLock } from 'lucide-react' ;
1212import type { PublicFolder } from '@opencoperlock/shared/client' ;
1313import { useT } from '@/lib/i18n' ;
1414
15+ interface Space {
16+ id : string ;
17+ name : string ;
18+ }
19+
1520export function FolderPicker ( {
1621 folders,
17- rootId,
18- rootName,
22+ spaces,
1923 excludeIds,
2024 currentId,
2125 count,
2226 onPick,
2327 onClose,
2428} : {
2529 folders : PublicFolder [ ] ;
26- rootId : string ;
27- rootName : string ;
30+ /** Selectable destination spaces (top-level folders compatible with the moved items). */
31+ spaces : Space [ ] ;
2832 excludeIds : Set < string > ;
2933 currentId : string | null ;
3034 count : number ;
3135 onPick : ( folderId : string ) => void ;
3236 onClose : ( ) => void ;
3337} ) {
3438 const { t } = useT ( ) ;
35- const [ cwd , setCwd ] = useState < string > ( rootId ) ;
39+ // With a single space, skip the picker step and open it straight away.
40+ const [ root , setRoot ] = useState < Space | null > ( spaces . length === 1 ? spaces [ 0 ] ! : null ) ;
41+ const [ cwd , setCwd ] = useState < string | null > ( spaces . length === 1 ? spaces [ 0 ] ! . id : null ) ;
3642 const byId = useMemo ( ( ) => new Map ( folders . map ( ( f ) => [ f . id , f ] ) ) , [ folders ] ) ;
43+
3744 const children = useMemo (
3845 ( ) =>
39- folders
40- . filter ( ( f ) => f . parentId === cwd && ! excludeIds . has ( f . id ) )
41- . sort ( ( a , b ) => a . name . localeCompare ( b . name ) ) ,
42- [ folders , cwd , excludeIds ] ,
46+ root
47+ ? folders
48+ . filter ( ( f ) => f . parentId === cwd && ! excludeIds . has ( f . id ) )
49+ . sort ( ( a , b ) => a . name . localeCompare ( b . name ) )
50+ : [ ] ,
51+ [ folders , cwd , excludeIds , root ] ,
4352 ) ;
53+
4454 const trail = useMemo ( ( ) => {
4555 const out : PublicFolder [ ] = [ ] ;
4656 let cur : string | null = cwd ;
47- while ( cur && cur !== rootId ) {
57+ while ( cur && cur !== root ?. id ) {
4858 const f = byId . get ( cur ) ;
4959 if ( ! f ) break ;
5060 out . unshift ( f ) ;
5161 cur = f . parentId ;
5262 }
5363 return out ;
54- } , [ cwd , rootId , byId ] ) ;
64+ } , [ cwd , root , byId ] ) ;
5565
5666 if ( typeof document === 'undefined' ) return null ;
57- const alreadyHere = cwd === currentId ;
58- const destName = trail . length ? trail [ trail . length - 1 ] ! . name : rootName ;
67+ const alreadyHere = ! ! root && cwd === currentId ;
68+ const destName = trail . length ? trail [ trail . length - 1 ] ! . name : root ?. name ?? '' ;
69+ const multi = spaces . length > 1 ;
70+
71+ function toSpaceList ( ) {
72+ setRoot ( null ) ;
73+ setCwd ( null ) ;
74+ }
5975
6076 return createPortal (
6177 < div className = "fixed inset-0 z-[130] flex items-center justify-center p-4" >
@@ -75,9 +91,19 @@ export function FolderPicker({
7591
7692 { /* breadcrumb */ }
7793 < div className = "flex flex-wrap items-center gap-1 border-b border-white/[0.05] px-5 py-2.5 text-sm text-zinc-400" >
78- < button className = "flex items-center gap-1 rounded px-1.5 py-0.5 transition hover:bg-white/5 hover:text-zinc-100" onClick = { ( ) => setCwd ( rootId ) } >
79- < Home size = { 14 } /> { rootName }
80- </ button >
94+ { multi && (
95+ < button className = "flex items-center gap-1 rounded px-1.5 py-0.5 transition hover:bg-white/5 hover:text-zinc-100" onClick = { toSpaceList } >
96+ < Home size = { 14 } /> { t ( 'copy.spaces' ) }
97+ </ button >
98+ ) }
99+ { root && (
100+ < span className = "flex items-center gap-1" >
101+ { multi && < ChevronRight size = { 13 } className = "text-zinc-600" /> }
102+ < button className = "flex items-center gap-1 rounded px-1.5 py-0.5 transition hover:bg-white/5 hover:text-zinc-100" onClick = { ( ) => setCwd ( root . id ) } >
103+ < FolderLock size = { 13 } /> { root . name }
104+ </ button >
105+ </ span >
106+ ) }
81107 { trail . map ( ( f ) => (
82108 < span key = { f . id } className = "flex items-center gap-1" >
83109 < ChevronRight size = { 13 } className = "text-zinc-600" />
@@ -88,9 +114,28 @@ export function FolderPicker({
88114 ) ) }
89115 </ div >
90116
91- { /* folder list */ }
117+ { /* body: space list (step 1) or folder list (step 2) */ }
92118 < div className = "min-h-[9rem] flex-1 overflow-y-auto p-2" >
93- { children . length === 0 ? (
119+ { ! root ? (
120+ spaces . length === 0 ? (
121+ < p className = "px-3 py-8 text-center text-sm text-zinc-500" > { t ( 'copy.noTargets' ) } </ p >
122+ ) : (
123+ spaces . map ( ( s ) => (
124+ < button
125+ key = { s . id }
126+ onClick = { ( ) => {
127+ setRoot ( s ) ;
128+ setCwd ( s . id ) ;
129+ } }
130+ className = "flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-zinc-200 transition hover:bg-white/[0.05]"
131+ >
132+ < FolderLock size = { 16 } className = "shrink-0 text-violet-300" />
133+ < span className = "min-w-0 flex-1 truncate" > { s . name } </ span >
134+ < ChevronRight size = { 15 } className = "shrink-0 text-zinc-500" />
135+ </ button >
136+ ) )
137+ )
138+ ) : children . length === 0 ? (
94139 < p className = "px-3 py-8 text-center text-sm text-zinc-500" > { t ( 'picker.noSubfolders' ) } </ p >
95140 ) : (
96141 children . map ( ( f ) => (
@@ -107,14 +152,16 @@ export function FolderPicker({
107152 ) }
108153 </ div >
109154
110- < div className = "flex items-center justify-between gap-3 border-t border-white/[0.07] px-5 py-3.5" >
111- < span className = "min-w-0 truncate text-xs text-zinc-500" >
112- { alreadyHere ? t ( 'picker.alreadyHere' ) : t ( 'picker.dest' , { name : destName } ) }
113- </ span >
114- < button className = "btn-primary shrink-0" disabled = { alreadyHere } onClick = { ( ) => onPick ( cwd ) } >
115- < Check size = { 15 } /> { t ( 'picker.moveHere' ) }
116- </ button >
117- </ div >
155+ { root && (
156+ < div className = "flex items-center justify-between gap-3 border-t border-white/[0.07] px-5 py-3.5" >
157+ < span className = "min-w-0 truncate text-xs text-zinc-500" >
158+ { alreadyHere ? t ( 'picker.alreadyHere' ) : t ( 'picker.dest' , { name : destName } ) }
159+ </ span >
160+ < button className = "btn-primary shrink-0" disabled = { alreadyHere } onClick = { ( ) => onPick ( cwd ?? root . id ) } >
161+ < Check size = { 15 } /> { t ( 'picker.moveHere' ) }
162+ </ button >
163+ </ div >
164+ ) }
118165 </ div >
119166 </ div > ,
120167 document . body ,
0 commit comments