a new section above Administration. it embeds THE PLATFORM'S OWN file browser rather than drawing a file list — upload, delete, rename, move, thumbnails, the context menu and every task hanging off it already exist and are maintained in one place. a plugin's own would be a second to keep in step, arriving without any of that. `basePath` IS A JAIL, not a starting folder: `useFileBrowserApp` resets currentPath whenever it leaves, so pointing it at `<root>/media/originals` means the breadcrumb stops there and `derived/` is not reachable by navigating. that is what was asked for — people work in originals, derived is made for them. IT IS A GUARDRAIL AND NOT A PERMISSION BOUNDARY, and the panel says so in its own docstring because the difference will matter to whoever reads this next. the file-browser API is unchanged and still addresses the whole home: anyone with file access reaches derived/ through the Files screen. this prevents an accident, not a determined person. confining it properly means scoping that API per user, which is a platform change and a much larger one. the panel also says, once, that derived/ exists and is made from these — otherwise somebody who knows about it would think it had been lost. `rootRelative` is new on the settings API because only the SERVER knows what home is. the file browser addresses everything relative to a root, and a browser deriving that prefix from the absolute path would be guessing. null when the root sits outside home — a legitimate setting the browser cannot address, which the panel explains rather than rendering an empty folder. NOTHING HERE IS FOFOS-SPECIFIC, which matters now that this plugin is the template for other site admins: the root is a setting, media/originals is the layout this plugin creates, and both are read from the API. another site's plugin changes the label. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
109 lines
4.6 KiB
TypeScript
109 lines
4.6 KiB
TypeScript
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
|
|
* `<root>/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<Settings | null>(null);
|
|
const [denied, setDenied] = useState(false);
|
|
|
|
const load = useCallback(async () => {
|
|
try {
|
|
setSettings(await clientRef.current.get<Settings>('/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 <Notice icon={TriangleAlert}>Only the owner can see where the site's media lives.</Notice>;
|
|
}
|
|
|
|
if (!settings) {
|
|
return <Notice icon={FolderOpen}>Loading…</Notice>;
|
|
}
|
|
|
|
/*
|
|
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 (
|
|
<Notice icon={TriangleAlert}>
|
|
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.
|
|
</Notice>
|
|
);
|
|
}
|
|
|
|
const base = `${settings.rootRelative === '/' ? '' : settings.rootRelative}/${ORIGINALS}`;
|
|
|
|
return (
|
|
<div className="flex size-full flex-col overflow-hidden">
|
|
{/* Said once, at the top, because the whole point of this panel is that it shows ONE of the two
|
|
folders. Without it, somebody who knows `derived/` exists would think it had been lost. */}
|
|
<p className="shrink-0 border-b border-border px-4 py-2 text-xs text-muted-foreground">
|
|
Uploads live here. The converted copies in <code>derived/</code> are made from these and are not edited by hand.
|
|
</p>
|
|
|
|
<div className="min-h-0 flex-1">
|
|
{/*
|
|
`panelId` so the folder somebody is in survives a reload — without it every refresh drops them
|
|
back at the top of originals, however deep they were.
|
|
*/}
|
|
<FileBrowserApp basePath={base} panelId="fofos-admin-media" />
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const Notice = ({ icon: Icon, children }: { icon: typeof FolderOpen; children: React.ReactNode }) => (
|
|
<div className="flex size-full flex-col items-center justify-center gap-2 p-6 text-center">
|
|
<Icon className="size-6 text-muted-foreground/60" />
|
|
<p className="max-w-sm text-sm text-muted-foreground">{children}</p>
|
|
</div>
|
|
);
|