import { Images } from 'lucide-react'; import type { FileAction } from 'officerdev'; /** * What this plugin adds to the file browser's context menu. * * ── Why an action and not a task ── * * A task runs a script as the owner and reports through the job system, which is right for "convert * these" and wrong for this. Creating a gallery is a name to type and a screen to land on — expressing * it as a job would produce something to watch whose only output is somewhere to go next. * * ── It only offers itself inside `originals/` ── * * A gallery item is a path relative to that folder, so the action is meaningless anywhere else — and a * menu entry that appears everywhere and works in one place is worse than one that appears where it * works. The check is on the path rather than on which panel is open, because the same browser is * mounted in several places and only the path says where it is pointed. */ const ORIGINALS = '/media/originals'; /** Everything under `originals/`, relative to it — which is what a gallery item stores. */ const relativeTo = (path: string): string | null => { const at = path.indexOf(ORIGINALS); return at === -1 ? null : path.slice(at + ORIGINALS.length).replace(/^\/+/, ''); }; export const fileActions: FileAction[] = [ { key: 'fofos-admin:create-gallery', label: 'Create Gallery', icon: Images, /* A folder, or any number of files — both are ways of saying "these, in this order". A mixed selection is refused: a folder inside a selection of files is either a mistake or a request to recurse, and neither is something to guess at from a right-click. */ appliesTo: (entries) => { if (!entries.length) return false; if (!entries.every((entry) => relativeTo(entry.path))) return false; const directories = entries.filter((entry) => entry.type === 'directory').length; return directories === 0 || (directories === 1 && entries.length === 1); }, run: (entries) => { const paths = entries.map((entry) => relativeTo(entry.path)).filter((path): path is string => Boolean(path)); /* Handed over in the URL, and the Galleries panel does the asking. The action has no dialog of its own on purpose: naming a gallery, seeing the slug it will get and watching the items land belongs on the screen that owns galleries, not in a modal over a file browser that would then have to explain where the result went. */ const params = new URLSearchParams(window.location.search); params.set('section', 'galleries'); params.set('create', entries.length === 1 && entries[0]!.type === 'directory' ? entries[0]!.name : ''); params.set('from', paths.join('\n')); window.history.pushState({}, '', `${window.location.pathname}?${params.toString()}`); // The router listens for this; pushState alone does not notify it. window.dispatchEvent(new PopStateEvent('popstate')); }, }, ];