/** * NextcloudFolderPicker — modal that lets the user navigate their * Nextcloud `files/` tree and register a subfolder as a SourceRoot in * mule-image. Lazy-loads each level via GET /nextcloud/browse?path=..., * so opening the picker doesn't slurp the whole tree. * * Path scoping is enforced server-side; we still avoid showing a `..` * affordance above the user's root so the UI never surfaces the idea * that there's something to escape to. */ import { useCallback, useEffect, useState } from 'react' import { ChevronRight, Folder, FolderOpen, Loader2, X } from 'lucide-react' import { nextcloud, type NextcloudBrowseEntry } from '../../services/api' import { toast } from '../ToastContainer' interface Props { open: boolean onClose: () => void onCreated: () => void // called after a successful POST so the parent refetches } export function NextcloudFolderPicker({ open, onClose, onCreated }: Props) { const [path, setPath] = useState('') const [entries, setEntries] = useState([]) const [parentRel, setParentRel] = useState(null) const [ncUser, setNcUser] = useState('') const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [submitting, setSubmitting] = useState(false) const [name, setName] = useState('') const loadPath = useCallback(async (target: string) => { setLoading(true) setError(null) try { const r = await nextcloud.browse(target) setEntries(r.entries) setParentRel(r.parent_rel) setNcUser(r.nc_username) setPath(r.rel_path) // Default the SourceRoot name from the leaf folder name. const leaf = r.rel_path.split('/').filter(Boolean).slice(-1)[0] if (leaf) setName(leaf) else setName(`${r.nc_username} library`) } catch (e) { const message = e instanceof Error ? e.message : String(e) setError(message) } finally { setLoading(false) } }, []) useEffect(() => { if (open) { void loadPath('') } }, [open, loadPath]) const submit = useCallback(async () => { if (!path) { toast.error('Pick a subfolder first — the root is too broad.') return } if (!name.trim()) { toast.error('Library name is required') return } setSubmitting(true) try { await nextcloud.createSourceRoot(name.trim(), path) toast.success(`Added "${name.trim()}" to your libraries`) onCreated() onClose() } catch (e) { const message = e instanceof Error ? e.message : String(e) toast.error(`Could not add library: ${message}`) } finally { setSubmitting(false) } }, [path, name, onCreated, onClose]) if (!open) return null // Breadcrumb segments for the current path. "/" rendered as the user's // root → click jumps back to that level. const segments = path ? path.split('/').filter(Boolean) : [] return (
Add library from Nextcloud
{/* Breadcrumb */}
{segments.map((seg, i) => { const target = segments.slice(0, i + 1).join('/') return ( ) })}
{/* List */}
{loading && (
Loading…
)} {error && !loading && (
{error}
)} {!loading && !error && entries.length === 0 && (
No subfolders here. Use Nextcloud to create a folder, then come back to register it.
)} {!loading && !error && entries.length > 0 && (
    {/* "Up one level" — only shown when not already at root */} {parentRel !== null && (
  • )} {entries.map((e) => (
  • ))}
)}
{/* Footer */}
Selected
{path ? `${ncUser}/files/${path}` : '(pick a subfolder)'}
setName(e.target.value)} placeholder="Library name" className="flex-1 rounded border border-border bg-surface px-2 py-1 text-sm" />
) }