import { useCallback, useEffect, useRef, useState } from 'react'; import { FolderOpen, TriangleAlert } from 'lucide-react'; import { FileBrowserApp } from 'officerdev'; import { useClient } from 'hooks/useClient'; /** * The site's own media, browsed in place. * * ── It embeds the platform's file browser rather than reimplementing one ── * * Upload, delete, rename, move, thumbnails, the context menu and every task that hangs off it already * exist and are maintained in one place. A plugin drawing its own file list would be a second one to * keep in step, and it would arrive without any of that. * * ── `basePath` is a jail, not a starting folder ── * * `useFileBrowserApp` resets `currentPath` whenever it leaves `basePath`, so pointing it at * `/media/originals` means the breadcrumb stops there and `derived/` is not reachable by * navigating. That is the behaviour the owner asked for: people work in originals, and derived is made * for them. * * **It is a guardrail, not a permission boundary, and the difference matters.** The file-browser API is * unchanged and still addresses the whole home — somebody with file access can reach `derived/` through * the Files screen or by hand. Confining it properly would mean scoping the API per user, which is a * platform change and a much larger one. What this prevents is an accident, not a determined person. * * ── Nothing here is fofos-specific ── * * The root is a setting, `media/originals` is the layout this plugin creates, and the panel reads both * from the API. Another site's admin plugin built from this one changes the label and nothing else. */ type Settings = { rootPath: string; rootRelative: string | null; isSet: boolean }; /** Where the uploads live, relative to the root. The other half, `derived/`, is deliberately not shown. */ const ORIGINALS = 'media/originals'; export const MediaPanel = () => { const client = useClient(); const clientRef = useRef(client); clientRef.current = client; const [settings, setSettings] = useState(null); const [denied, setDenied] = useState(false); const load = useCallback(async () => { try { setSettings(await clientRef.current.get('/fofos-admin/_config/root')); } catch (failure) { // The root is owner-only. A member sees the section and is told why rather than an empty browser. if ((failure as { status?: number })?.status === 403) setDenied(true); else setSettings({ rootPath: '', rootRelative: null, isSet: false }); } }, []); useEffect(() => { void load(); }, [load]); if (denied) { return Only the owner can see where the site's media lives.; } if (!settings) { return Loading…; } /* A root outside the owner's home is a legitimate setting and the file browser cannot address it — everything it does is relative to `home`. Said plainly here rather than rendering an empty folder, which would read as "there is nothing in your media". */ if (!settings.rootRelative) { return ( The root location is outside your home directory, so the file browser cannot show it. Move it under your home in Administration, or use the Files screen. ); } const base = `${settings.rootRelative === '/' ? '' : settings.rootRelative}/${ORIGINALS}`; /* Just the browser. No banner explaining `derived/`. The people using this upload photographs; the two-folder split is the machine's business, not theirs. A line at the top of every visit is a permanent tax paid by everyone to answer a question almost nobody asks — and it is written properly in the affordance and the plugin's docs. `rootLabel` so the breadcrumb reads "Media" rather than "originals": the folder's name on disk is an implementation detail of how this plugin lays out a root, and the section is called Media. `panelId` so the folder somebody is in survives a reload — without it every refresh drops them back at the top, however deep they were. */ return ; }; const Notice = ({ icon: Icon, children }: { icon: typeof FolderOpen; children: React.ReactNode }) => (

{children}

);