Skip to content

Commit 7f5e25c

Browse files
feat(drive): re-click 'Mes Espaces' returns to spaces list + multi-space Move
- Clicking the 'Mes Espaces' nav item while already on /drive now pops back to the spaces list (the URL is unchanged, so the drive page listens for a small window event dispatched by the nav link). - The Move picker is now multi-space: first pick a destination space, then navigate its tree. Destinations are filtered to spaces compatible with the source (non-ZK, same public/normal mode) so a move never mixes encryption modes; the server already accepts any owned folder as the target.
1 parent 7997899 commit 7f5e25c

3 files changed

Lines changed: 95 additions & 33 deletions

File tree

‎apps/web/src/app/(app)/drive/page.tsx‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,13 @@ export default function EspacesPage() {
315315
setAnchorKey(null);
316316
}, [currentFolderId, activeSpaceId]);
317317

318+
// Clicking the "Mes Espaces" nav item while already on /drive should pop back to the spaces list.
319+
useEffect(() => {
320+
const back = () => leaveSpace();
321+
window.addEventListener('ocl:drive-home', back);
322+
return () => window.removeEventListener('ocl:drive-home', back);
323+
}, []);
324+
318325
async function enterSpace(space: PublicFolder) {
319326
setError(null);
320327
setActiveSpaceId(space.id);
@@ -1955,8 +1962,9 @@ export default function EspacesPage() {
19551962
{movePicker && activeSpace && (
19561963
<FolderPicker
19571964
folders={allFolders}
1958-
rootId={activeSpace.id}
1959-
rootName={activeSpace.name}
1965+
spaces={allFolders
1966+
.filter((f) => f.parentId === null && !f.isZeroKnowledge && f.isPublic === activeSpace.isPublic)
1967+
.map((f) => ({ id: f.id, name: f.name }))}
19601968
excludeIds={movePicker.exclude}
19611969
currentId={movePicker.currentId}
19621970
count={movePicker.items.length}

‎apps/web/src/app/(app)/layout.tsx‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
5858
return (
5959
<Link
6060
href={href}
61+
onClick={() => {
62+
// Re-clicking "Mes Espaces" while already in /drive pops back to the spaces list
63+
// (the URL doesn't change, so the drive page listens for this instead).
64+
if (href === '/drive' && pathname === '/drive') {
65+
window.dispatchEvent(new Event('ocl:drive-home'));
66+
}
67+
}}
6168
className={`flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition ${
6269
active ? 'bg-accent-soft text-violet-300' : 'text-zinc-400 hover:bg-white/5 hover:text-zinc-100'
6370
}`}

‎apps/web/src/components/drive/FolderPicker.tsx‎

Lines changed: 78 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,77 @@
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
*/
99
import { useMemo, useState } from 'react';
1010
import { 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';
1212
import type { PublicFolder } from '@opencoperlock/shared/client';
1313
import { useT } from '@/lib/i18n';
1414

15+
interface Space {
16+
id: string;
17+
name: string;
18+
}
19+
1520
export 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

Comments
 (0)