the previous commit claimed to remove the explanatory line and did not. prettier had reflowed that JSX onto one line, my multi-line match missed, and the commit carried only the version bump — which I then reported as done. the banner was still on fofos and the owner saw it. same shape as the git-add failure earlier today: a command that did not do what I asked, and no check afterwards. verified by grep this time before committing — 0 occurrences. and the breadcrumb now reads Media rather than originals, through `rootLabel` on the platform's FileBrowserApp. the folder's name on disk is how this plugin lays out a root; the person looking at it never chose that word. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
106 lines
4.5 KiB
TypeScript
106 lines
4.5 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}`;
|
|
|
|
/*
|
|
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 <FileBrowserApp basePath={base} rootLabel="Media" panelId="fofos-admin-media" />;
|
|
};
|
|
|
|
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>
|
|
);
|