an embedded browser can put its folder in a query param

`urlPath` was the wrong tool and I reached for it because it was there. it owns the
URL's PATHNAME — reads the folder out of `location.pathname`, navigates to
`/files/<folder>` — which is correct for the Files screen, because that screen IS
that route, and catastrophic in a panel: opening a folder in the fofos Media section
took the whole app to /files.

so an embedder gets its own mechanism rather than a shared one bent to fit.
`pathParam` names a SEARCH param and never touches the pathname. three sources of
the current folder now, mutually exclusive by construction: the pathname for /files,
a named query param for an embedder, component state for an ordinary panel.

two details that are not obvious and would each be a bug:

a param naming a folder OUTSIDE `basePath` is ignored rather than obeyed. it arrives
from a URL somebody may have edited, and the jail is the whole promise of an
embedded browser — a shared link must not be a way through it.

the base folder is the ABSENCE of the param, not a value. so a link to the top of an
embedded browser is the plain screen URL, and nothing accumulates in the query string
for having done nothing.

written with `replace`, because browsing folders is not navigation anybody wants
twenty back-button presses of.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-29 09:01:41 +01:00
co-authored by Claude Opus 5
parent cad0ea3c9c
commit 1da58e5231
2 changed files with 35 additions and 3 deletions
@@ -13,6 +13,13 @@ type FileBrowserAppProps = {
rootOverride?: string;
/** What the root crumb is called. Defaults to the base folder's own name. */
rootLabel?: string;
/**
* Keep the current folder in this query param, on the route you are already on.
*
* For an embedded browser that wants a reloadable, shareable location. NOT `urlPath`, which owns the
* pathname and navigates to `/files` — passing that from a panel takes the whole app with it.
*/
pathParam?: string;
/** Put the current folder in `?path=`. Opt-in — only the /files screen is guaranteed one browser. */
urlPath?: boolean;
/**
@@ -22,8 +29,15 @@ type FileBrowserAppProps = {
panelId?: string;
};
export const FileBrowserApp = ({ basePath = '/', rootOverride, rootLabel, urlPath, panelId }: FileBrowserAppProps) => {
const fileBrowserManager = useFileBrowserApp(basePath, rootOverride, urlPath, panelId);
export const FileBrowserApp = ({
basePath = '/',
rootOverride,
rootLabel,
urlPath,
panelId,
pathParam,
}: FileBrowserAppProps) => {
const fileBrowserManager = useFileBrowserApp(basePath, rootOverride, urlPath, panelId, pathParam);
const { handleNavigate } = fileBrowserManager;
return (
@@ -24,7 +24,25 @@ import { useTranslation } from '../../../locales';
*/
export const FILES_PATH_PARAM = 'path';
export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPath = false, panelId?: string) => {
/**
* `pathParam` — keep the current folder in a QUERY PARAM, on whatever route we are already on.
*
* This is not `urlPath` and must not be confused with it. `urlPath` owns the URL's PATHNAME: it reads
* the folder out of `location.pathname` and navigates to `/files/<folder>`. That is correct for the
* Files screen, which IS that route, and catastrophic inside a panel — a plugin passing `urlPath`
* navigated the whole app to /files the moment anybody opened a folder.
*
* So an embedder gets its own mechanism: a named search param, written with `replace` so browsing does
* 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.
*/
export const useFileBrowserApp = (
basePath: string,
rootOverride?: string,
urlPath = false,
panelId?: string,
pathParam?: string,
) => {
const { t } = useTranslation();
const { user } = useAuth();
const navigate = useNavigate();