the file browser can be a file picker

A prop, not a second component. `onPickFile` turns opening a file into handing it
back rather than opening a viewer; folders still navigate, because choosing a
file means getting to it.

Building a picker separately would mean reimplementing a listing, a breadcrumb,
sorting, thumbnails, upload and keyboard handling — all of it free to drift from
the real browser, and none of it better. fofos-admin needs one to choose media
and it is the first of several, so this belongs here rather than in the plugin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-29 20:30:22 +01:00
co-authored by Claude Opus 5
parent 9719e36723
commit e756007551
4 changed files with 33 additions and 2 deletions
@@ -6,7 +6,7 @@ import { TaskRunnerDialog } from './components/TaskRunnerDialog';
import { AgentRunnerDialog } from './components/AgentRunnerDialog';
import { VideoDownloadDialog } from './components/VideoDownloadDialog';
import { DictateDialog } from './components/DictateDialog';
import { useFileBrowserApp } from './useFileBrowserApp';
import { useFileBrowserApp, type PickedFile } from './useFileBrowserApp';
type FileBrowserAppProps = {
basePath?: string;
@@ -27,6 +27,14 @@ type FileBrowserAppProps = {
* where the folder is the URL and already does.
*/
panelId?: string;
/**
* Turn this into a CHOOSER: opening a file hands it back instead of opening a viewer.
*
* Folders still navigate, because choosing a file means getting to it. A prop rather than a second
* component — a picker built separately is a listing, a breadcrumb, a sort, thumbnails and keyboard
* handling reimplemented, all of it free to drift from the real browser.
*/
onPickFile?: (file: PickedFile) => void;
};
export const FileBrowserApp = ({
@@ -36,8 +44,9 @@ export const FileBrowserApp = ({
urlPath,
panelId,
pathParam,
onPickFile,
}: FileBrowserAppProps) => {
const fileBrowserManager = useFileBrowserApp(basePath, rootOverride, urlPath, panelId, pathParam);
const fileBrowserManager = useFileBrowserApp(basePath, rootOverride, urlPath, panelId, pathParam, onPickFile);
const { handleNavigate } = fileBrowserManager;
return (
@@ -36,12 +36,17 @@ export const FILES_PATH_PARAM = 'path';
* not fill the back stack, and no pathname involved at all. A reload lands where you were and the link
* can be sent to somebody, which is what was actually wanted.
*/
/** What a picker is handed when somebody opens a file in pick mode. */
export type PickedFile = { name: string; path: string; type: string };
export const useFileBrowserApp = (
basePath: string,
rootOverride?: string,
urlPath = false,
panelId?: string,
pathParam?: string,
/** Set to turn this into a chooser: opening a file calls this instead of a viewer. */
onPickFile?: (file: PickedFile) => void,
) => {
const { t } = useTranslation();
const { user } = useAuth();
@@ -475,6 +480,21 @@ export const useFileBrowserApp = (
setCurrentPath(next);
return;
}
/*
PICK MODE.
When a caller is using this browser to CHOOSE a file rather than to work in a folder, opening a
file means handing it back — not opening a viewer the caller would then have to close. Folders
still navigate above, because choosing a file requires getting to it.
A prop rather than a second browser: a picker built separately would be a listing, a breadcrumb,
a sort, thumbnails and keyboard handling reimplemented, all of it drifting from the real one.
This is the same component with one branch.
*/
if (onPickFile) {
onPickFile({ name: entry.name, path: entryPath(entry.name), type: entry.type });
return;
}
// An archive extracts rather than opening a pane. The viewer has nothing to show for one — it renders
// a placeholder reading "Archive file" and the name you can already see in the listing — so a pane
// was a step on the way to the only thing anyone opens a zip for. Extract resolves collisions, so a
@@ -9,6 +9,7 @@ export { useTasks, type TaskSummary } from './useTasks';
export { useRecentFiles } from './useRecentFiles';
export { usePinnedFiles } from './usePinnedFiles';
export { FileBrowserApp, FileBrowserPanelWrapper } from './FileBrowserApp';
export type { PickedFile } from './FileBrowserApp/useFileBrowserApp';
export { FileBrowserWidget } from './FileBrowserWidget';
export { TaskRunnerModal } from './FileBrowserApp/components/TaskRunnerModal';
+1
View File
@@ -59,6 +59,7 @@ export {
useRecentFiles,
usePinnedFiles,
FileBrowserApp,
type PickedFile,
FileBrowserPanelWrapper,
FileBrowserWidget,
TaskRunnerModal,