mono/packages/ui/src/modules/storage/file-browser-commands.ts
2026-04-07 18:59:02 +02:00

142 lines
4.6 KiB
TypeScript

import type { LucideIcon } from 'lucide-react';
import type { Action } from '@/actions/types';
import type { SortKey } from '@/modules/storage/types';
/**
* Zustand {@link Action} ids — use {@link vfsPanelActionId} so paths stay consistent.
*
* **Testing:**
* - Store `id` includes `panelId` → unique per panel; use `getActionsByPath(\`vfs/panel/${id}/\`)` in unit tests when you control the panel id.
* - **E2E:** prefer stable **`data-testid`** on ribbon/context items using {@link vfsActionTestId} (no panel segment) so selectors do not depend on dynamic panel ids.
*/
export const VFS_ACTION_PREFIX = 'vfs/panel' as const;
/** Stable slug (last path segment) for each command — use in tests and {@link vfsPanelActionId}. */
export const VfsActionSlug = {
navigateUp: 'navigate-up',
refresh: 'refresh',
open: 'open',
download: 'download',
copy: 'copy',
sort: 'sort',
viewList: 'view-list',
viewThumbs: 'view-thumbs',
viewTree: 'view-tree',
zoomIn: 'zoom-in',
zoomOut: 'zoom-out',
toggleExplorer: 'toggle-explorer',
togglePreview: 'toggle-preview',
filter: 'filter',
search: 'search',
clearSearch: 'clear-search',
/** Krusader-style: new tab on this side, cloned from current folder */
newTab: 'new-tab',
/** Switch file browser to dual-pane layout */
dualLayout: 'dual-layout',
/** Back to single-pane layout */
singleLayout: 'single-layout',
/** Mirror left navigation to right pane (dual mode) */
linkPanes: 'link-panes',
/** In-app fullscreen: hide app nav/footer for the file browser shell */
appFullscreen: 'app-fullscreen',
} as const;
export type VfsActionSlugKey = keyof typeof VfsActionSlug;
/** Full action id in the global store (unique per panel). */
export function vfsPanelActionId(panelId: string, slug: string): string {
return `${VFS_ACTION_PREFIX}/${panelId}/${slug}`;
}
/**
* Stable `data-testid` for DOM (ribbon, menus) — **no** panelId, safe for e2e.
* Pattern: `vfs-action-<slug>`
*/
export function vfsActionTestId(slug: string): string {
return `vfs-action-${slug.replace(/[^a-z0-9-]/gi, '_')}`;
}
/**
* Primitive signature for Zustand selectors — subscribe to this instead of the full `actions` map
* so unrelated store updates and register/unregister cycles do not re-render every consumer.
*/
export function vfsPanelActionStoreSignature(
actions: Record<string, Action>,
panelId: string,
visibility: 'Ribbon' | 'ContextMenu',
): string {
const prefix = `${VFS_ACTION_PREFIX}/${panelId}/`;
return Object.values(actions)
.filter((a) => {
if (!a.id.startsWith(prefix)) return false;
if (visibility === 'Ribbon') return a.visibilities?.Ribbon !== false;
return a.visibilities?.ContextMenu !== false;
})
.map(
(a) =>
`${a.id}:${String(a.disabled)}:${String(a.metadata?.active ?? '')}:${String(a.metadata?.ribbonTab ?? '')}`,
)
.sort()
.join('|');
}
/** Which ribbon tab an action belongs to (Explorer-style Home vs View). */
export type VfsRibbonTabId = 'home' | 'view';
/**
* Command surface for the active file browser panel — consumed by {@link FileBrowserRibbonBar}
* and (later) context menus and plugin automation. Keep fields stable; extend with optional blocks.
*/
export interface FileBrowserPanelCommandApi {
panelId: string;
mount: string;
path: string;
selectionCount: number;
canGoUp: boolean;
goUp: () => void;
refresh: () => void;
canOpen: boolean;
openSelected: () => void;
allowDownload: boolean;
download: () => void;
downloadFolder?: () => void;
canCopy: boolean;
copy: () => void;
sortBy: SortKey;
sortAsc: boolean;
cycleSort: () => void;
viewMode: 'list' | 'thumbs' | 'tree';
setViewMode: (m: 'list' | 'thumbs' | 'tree') => void;
zoomIn: () => void;
zoomOut: () => void;
showExplorer: boolean;
toggleExplorer: () => void;
showPreview: boolean;
togglePreview: () => void;
openFilter: () => void;
openSearch: () => void;
isSearchMode: boolean;
clearSearch: () => void;
}
/** Future: extension commands (open-with, automation pipes) keyed by group id */
export interface FileBrowserPluginCommand {
id: string;
label: string;
icon?: LucideIcon;
/** Ribbon group id, e.g. "automate" | "open-with" */
group: string;
order?: number;
disabled?: boolean;
run: (api: FileBrowserPanelCommandApi) => void | Promise<void>;
}