Files
fofos-admin/web/file-actions.ts
T
pastilhasandClaude Opus 5 f61653f7af Galleries: a panel, and Create Gallery in the file browser
The section reads the site's own admin API, so it requires a CONNECTION where
Media beside it requires only a root. The difference is real rather than
copy-paste: Media is a file browser over a folder on this disk, while a gallery
is a row in the site's database — opening it without the site answering would be
a screen where every action fails.

The context action hands its selection over in the URL and sends you to the
panel rather than opening a dialog of its own. 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.

It offers itself only inside originals/, because a gallery item is a path
relative to that folder and an entry that appears everywhere while working in one
place is worse than one that appears where it works. The test is on the PATH, not
on which panel is open — the same browser is mounted in several places and only
the path says where it is pointed.

A mixed selection is refused. A folder among files is either a mistake or a
request to recurse, and neither is something to guess from a right-click.

The handoff is consumed once and stripped from the URL. Left there, a refresh
would create the gallery again, which somebody discovers as four copies.

Nothing is written until Create: the folder's name is a suggestion in the field,
not a decision already taken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 21:08:25 +01:00

65 lines
2.9 KiB
TypeScript

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'));
},
},
];