{inlineActions.map(({ action, isDisabled }) => (
@@ -221,12 +224,13 @@ const BulkActionButton = ({ action, isDisabled, onClick }: BulkActionButtonProps
const Icon = action.icon as ComponentType<{ className?: string }> | undefined;
const button = (
);
diff --git a/frontend/src/components/shared/file-manager/file-manager-row.tsx b/frontend/src/components/shared/file-manager/file-manager-row.tsx
index 6192e0f1..ffc7737d 100644
--- a/frontend/src/components/shared/file-manager/file-manager-row.tsx
+++ b/frontend/src/components/shared/file-manager/file-manager-row.tsx
@@ -153,9 +153,7 @@ const buildVisibleActions = (
actions: readonly FileManagerAction[],
file: FileManagerInternalNode,
): FileManagerAction[] =>
- actions.filter((action) =>
- file.isDir ? action.appliesToDirs === true : action.appliesToFiles !== false,
- );
+ actions.filter((action) => (file.isDir ? action.appliesToDirs === true : action.appliesToFiles !== false));
const FileManagerRowImpl = ({
actions,
@@ -425,7 +423,18 @@ const FileManagerRowImpl = ({
onToggleExpand(file.path, isExpanded)}
+ onClick={() => {
+ // Mirror the double-click / Enter semantics here so the
+ // chevron stays consistent with the row-level "open"
+ // gesture: navigation-style consumers (e.g. the remote
+ // container browser) drill into the folder instead of
+ // toggling expansion that has no children to show.
+ if (onOpenDirectory) {
+ onOpenDirectory(file);
+ } else {
+ onToggleExpand(file.path, isExpanded);
+ }
+ }}
{...skipRowClickProps}
>
diff --git a/frontend/src/components/shared/file-manager/file-manager-types.ts b/frontend/src/components/shared/file-manager/file-manager-types.ts
index bab3b625..5bdb24b3 100644
--- a/frontend/src/components/shared/file-manager/file-manager-types.ts
+++ b/frontend/src/components/shared/file-manager/file-manager-types.ts
@@ -13,8 +13,8 @@ export interface FileManagerAction {
appliesToDirs?: boolean;
/**
* When true (default), the action is shown for file rows. Set to `false` for
- * actions that only make sense on directory rows (e.g. "Upload files here",
- * "New folder here") combined with `appliesToDirs: true`.
+ * actions that only make sense on directory rows (e.g. "Upload files",
+ * "New folder") combined with `appliesToDirs: true`.
*/
appliesToFiles?: boolean;
/**
@@ -242,15 +242,12 @@ export interface FileManagerProps {
*/
onOpen?: (file: FileNode) => void;
/**
- * Fired when the user "opens" a *directory* row via double-click or `Enter`.
- * When provided, **replaces** the default expand/collapse gesture — useful
- * for navigation-style file browsers (e.g. drilling into a remote directory
- * by replacing the listing instead of expanding inline). When omitted,
- * directories keep the default expand/collapse behaviour.
- *
- * The chevron icon on the row's left edge always toggles expand/collapse
- * regardless of this prop, so the user still has access to inline
- * exploration when it makes sense.
+ * Fired when the user "opens" a *directory* row via double-click, `Enter`,
+ * or a click on the row's chevron. When provided, **replaces** the default
+ * expand/collapse gesture — useful for navigation-style file browsers
+ * (e.g. drilling into a remote directory by replacing the listing instead
+ * of expanding inline). When omitted, directories keep the default
+ * expand/collapse behaviour for all three gestures.
*/
onOpenDirectory?: (dir: FileNode) => void;
/**
diff --git a/frontend/src/components/shared/file-manager/file-manager-utils.test.ts b/frontend/src/components/shared/file-manager/file-manager-utils.test.ts
index 0e4d100f..e19ef7d0 100644
--- a/frontend/src/components/shared/file-manager/file-manager-utils.test.ts
+++ b/frontend/src/components/shared/file-manager/file-manager-utils.test.ts
@@ -513,6 +513,58 @@ describe('toggleSubtreeOnSet', () => {
expect(next).not.toBe(prev);
expect([...prev]).toEqual(['x']);
});
+
+ it('REGRESSION: with `rootPath`, clears the branch when every descendant is selected even though the dir itself is missing from prev', () => {
+ // User scenario: inside an expanded folder, select every file via the
+ // row checkboxes one by one (or via the header "select all" while the
+ // folder is the only visible group). The folder's own path is NEVER
+ // added to `selectedPaths` — the tri-state still renders "checked"
+ // because `computeDirSelectionState` only counts descendants. A single
+ // click on the folder's checkbox must clear the branch; without
+ // `rootPath` the strict `isEverySelected` check would treat the dir's
+ // missing path as "not all selected" and ADD it instead, requiring a
+ // second click to actually deselect.
+ expect([...toggleSubtreeOnSet(new Set(['dir/a', 'dir/b']), ['dir', 'dir/a', 'dir/b'], 'dir')]).toEqual([]);
+ });
+
+ it('with `rootPath`, still adds the missing pieces when only some descendants are selected', () => {
+ expect([...toggleSubtreeOnSet(new Set(['dir/a']), ['dir', 'dir/a', 'dir/b'], 'dir')].sort()).toEqual([
+ 'dir',
+ 'dir/a',
+ 'dir/b',
+ ]);
+ });
+
+ it('with `rootPath`, treats a fully selected branch (dir + descendants) the same as descendants-only', () => {
+ // Both shapes ("dir + every descendant" and "every descendant, no dir")
+ // render the checkbox as fully checked, so a single click must clear
+ // the entire branch in either case.
+ expect([...toggleSubtreeOnSet(new Set(['dir', 'dir/a', 'dir/b']), ['dir', 'dir/a', 'dir/b'], 'dir')]).toEqual(
+ [],
+ );
+ });
+
+ it('with `rootPath` on an empty folder (paths === [folder]), falls back to a simple binary toggle of the folder itself', () => {
+ // Empty folder: `paths.length === 1`, the descendant-only short-circuit
+ // would treat "no descendants" as vacuously all-selected, which would
+ // make every click on an unselected empty folder remove its own path
+ // (a no-op). Falling through to the strict path keeps the binary
+ // semantics that match `computeDirSelectionState` for empty folders.
+ expect([...toggleSubtreeOnSet(new Set(), ['empty'], 'empty')]).toEqual(['empty']);
+ expect([...toggleSubtreeOnSet(new Set(['empty']), ['empty'], 'empty')]).toEqual([]);
+ });
+
+ it('with `rootPath`, preserves unrelated entries on both add and remove', () => {
+ expect([...toggleSubtreeOnSet(new Set(['dir/a', 'x']), ['dir', 'dir/a', 'dir/b'], 'dir')].sort()).toEqual([
+ 'dir',
+ 'dir/a',
+ 'dir/b',
+ 'x',
+ ]);
+ expect([...toggleSubtreeOnSet(new Set(['dir/a', 'dir/b', 'x']), ['dir', 'dir/a', 'dir/b'], 'dir')]).toEqual([
+ 'x',
+ ]);
+ });
});
describe('computeRowClickSelection — single modifier', () => {
@@ -612,6 +664,26 @@ describe('computeRowClickSelection — toggle modifier', () => {
expect([...result.next]).toEqual(['unrelated']);
expect(result.nextAnchor).toBe('dir');
});
+
+ it('REGRESSION: cmd-click on a folder whose descendants are all selected (dir itself missing) clears the branch in one gesture', () => {
+ // Mirror of the checkbox-toggle regression — `Cmd`/`Ctrl`+click on a
+ // folder row goes through the same `toggleSubtreeOnSet` path. If the
+ // user single-clicked each child to fill the branch (so `dir`'s own
+ // path was never added), a follow-up cmd-click on the folder row must
+ // strip everything in one gesture, not silently add `dir` and require
+ // a second click.
+ const result = computeRowClickSelection({
+ anchor: null,
+ flatVisible: ['dir', 'dir/a', 'dir/b'],
+ modifier: 'toggle',
+ path: 'dir',
+ prev: new Set(['dir/a', 'dir/b', 'unrelated']),
+ subtreePaths: ['dir', 'dir/a', 'dir/b'],
+ });
+
+ expect([...result.next]).toEqual(['unrelated']);
+ expect(result.nextAnchor).toBe('dir');
+ });
});
describe('computeRowClickSelection — range modifier', () => {
@@ -1345,6 +1417,24 @@ describe('computeToggleSelection (Space / row checkbox)', () => {
it('treats empty subtreePaths as "single path" (defensive)', () => {
expect([...computeToggleSelection({ path: 'a', prev: new Set(), subtreePaths: [] })]).toEqual(['a']);
});
+
+ it('REGRESSION: clears the branch in one click when every descendant is selected but the dir itself was never added', () => {
+ // Real-world: user opens `dir`, ticks each child checkbox (or the
+ // header's "select all" with `dir` being the only visible group). The
+ // folder's tri-state shows "checked" because every descendant is in
+ // the selection — but `dir` itself isn't. A single click on the
+ // folder checkbox must DESELECT the whole branch; the previous code
+ // required two clicks (the first one silently added `dir`'s own path,
+ // the second one finally removed everything because by then the
+ // strict "every path including dir" check passed).
+ expect([
+ ...computeToggleSelection({
+ path: 'dir',
+ prev: new Set(['dir/a', 'dir/b']),
+ subtreePaths: ['dir', 'dir/a', 'dir/b'],
+ }),
+ ]).toEqual([]);
+ });
});
describe('computeToggleSelectAll', () => {
diff --git a/frontend/src/components/shared/file-manager/file-manager-utils.ts b/frontend/src/components/shared/file-manager/file-manager-utils.ts
index a0b53693..fe13cd30 100644
--- a/frontend/src/components/shared/file-manager/file-manager-utils.ts
+++ b/frontend/src/components/shared/file-manager/file-manager-utils.ts
@@ -515,13 +515,47 @@ export const isEverySelected = (paths: Iterable, selected: ReadonlySet, paths: readonly string[]): Set => {
+export const toggleSubtreeOnSet = (
+ prev: ReadonlySet,
+ paths: readonly string[],
+ rootPath?: string,
+): Set => {
const next = new Set(prev);
- if (isEverySelected(paths, next)) {
+ let allSelected: boolean;
+
+ if (rootPath !== undefined && paths.length > 1) {
+ allSelected = true;
+
+ for (const p of paths) {
+ if (p === rootPath) {
+ continue;
+ }
+
+ if (!next.has(p)) {
+ allSelected = false;
+ break;
+ }
+ }
+ } else {
+ allSelected = isEverySelected(paths, next);
+ }
+
+ if (allSelected) {
removeAllFromSet(next, paths);
} else {
addAllToSet(next, paths);
@@ -620,7 +654,7 @@ export const computeRowClickSelection = ({
if (modifier === 'toggle') {
if (hasSubtree && subtreePaths) {
- return { next: toggleSubtreeOnSet(prev, subtreePaths), nextAnchor: path };
+ return { next: toggleSubtreeOnSet(prev, subtreePaths, path), nextAnchor: path };
}
const next = new Set(prev);
@@ -695,7 +729,7 @@ interface ComputeToggleSelectionArgs {
*/
export const computeToggleSelection = ({ path, prev, subtreePaths }: ComputeToggleSelectionArgs): Set => {
if (subtreePaths && subtreePaths.length > 0) {
- return toggleSubtreeOnSet(prev, subtreePaths);
+ return toggleSubtreeOnSet(prev, subtreePaths, path);
}
const next = new Set(prev);
diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx
index 5c3da3aa..4967d4e4 100644
--- a/frontend/src/components/ui/dialog.tsx
+++ b/frontend/src/components/ui/dialog.tsx
@@ -61,7 +61,7 @@ DialogHeader.displayName = 'DialogHeader';
const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
);
diff --git a/frontend/src/components/ui/sheet.tsx b/frontend/src/components/ui/sheet.tsx
index a968a7d0..15aef09e 100644
--- a/frontend/src/components/ui/sheet.tsx
+++ b/frontend/src/components/ui/sheet.tsx
@@ -83,7 +83,7 @@ SheetHeader.displayName = 'SheetHeader';
const SheetFooter = ({ className, ...props }: React.HTMLAttributes) => (
);
diff --git a/frontend/src/features/flows/files/flow-files-attach-resources-dialog.tsx b/frontend/src/features/flows/files/flow-files-attach-resources-dialog.tsx
index 0bba43d0..9c9538c9 100644
--- a/frontend/src/features/flows/files/flow-files-attach-resources-dialog.tsx
+++ b/frontend/src/features/flows/files/flow-files-attach-resources-dialog.tsx
@@ -6,7 +6,14 @@ import { OverwriteConfirmDialog } from '@/components/shared/overwrite-confirm-di
import { OverwriteCtaButtons } from '@/components/shared/overwrite-cta-buttons';
import { useOverwriteAction } from '@/components/shared/use-overwrite-action';
import { Button } from '@/components/ui/button';
-import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty';
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '@/components/ui/input-group';
import { toFileNode } from '@/features/resources/resources-utils';
@@ -174,11 +181,11 @@ const FlowFilesAttachResourcesDialogBody = ({
return (
<>
-
-
+
+
- Attach resources from library
+ Attach resources
Pick files and/or folders from your global library — they will be copied into{' '}
@@ -218,7 +225,7 @@ const FlowFilesAttachResourcesDialogBody = ({
) : (
-
-
+
+
{selectedCount > 0
? `${selectedCount} selected`
: hasResources
? 'Select one or more items'
: ''}
-
+
-
+ (
+ () => files.map((file) => ({ ...file, id: file.path, path: file.name })),
+ [files],
+ );
+
+ /**
+ * Reverse lookup `name → absolute container path`, used to map the
+ * FileManager's name-keyed selection back to the absolute paths the
+ * backend's pull endpoint expects.
+ */
+ const nameToAbsolutePath = useMemo(() => {
+ const map = new Map();
+
+ for (const file of files) {
+ map.set(file.name, file.path);
+ }
+
+ return map;
+ }, [files]);
+
const { isPulling, pull } = useFlowFilesPull({
flowId,
// Refresh the listing after a successful pull so newly available entries
@@ -146,7 +183,11 @@ const FlowFilesPullDialogForm = ({ cachedFiles, flowId, onClose, onSuccess }: Fl
const handleOpenDirectory = useCallback(
(dir: FileNode) => {
- navigateTo(dir.path);
+ // `dir.id` is the absolute container path (we flattened the
+ // listing into `flatFiles` for the FileManager). `dir.path` is
+ // just the entry's name in this dialog and would normalise to
+ // a wrong absolute path (`/${name}`) if we used it directly.
+ navigateTo(dir.id);
},
[navigateTo],
);
@@ -170,15 +211,28 @@ const FlowFilesPullDialogForm = ({ cachedFiles, flowId, onClose, onSuccess }: Fl
}, [refetchListing]);
// Final list of paths to pull. Empty selection → fall back to the directory
- // the user is currently browsing. Non-empty selection wins and is deduped
- // so a folder + one of its descendants don't double-process.
+ // the user is currently browsing. Non-empty selection wins and is mapped
+ // back from the FileManager's name-keyed selection to absolute container
+ // paths, then deduped so a folder + one of its descendants don't
+ // double-process. (Dedup is mostly defensive in this dialog because the
+ // listing is single-level, so descendants aren't visible.)
const pullTargets = useMemo(() => {
- if (selectedPaths.size > 0) {
- return dedupeOverlappingPaths(selectedPaths);
+ if (selectedPaths.size === 0) {
+ return [currentPath];
}
- return [currentPath];
- }, [currentPath, selectedPaths]);
+ const absolutePaths: string[] = [];
+
+ for (const name of selectedPaths) {
+ const absolute = nameToAbsolutePath.get(name);
+
+ if (absolute) {
+ absolutePaths.push(absolute);
+ }
+ }
+
+ return dedupeOverlappingPaths(absolutePaths);
+ }, [currentPath, nameToAbsolutePath, selectedPaths]);
const isUpDisabled = currentPath === '/' || isListingLoading || isPulling;
const isPullDisabled = isListingLoading || pullTargets.length === 0 || !flowId;
@@ -237,7 +291,7 @@ const FlowFilesPullDialogForm = ({ cachedFiles, flowId, onClose, onSuccess }: Fl
Browse the running container and select files or directories to sync into the local cache under{' '}
- container/. Double-click a folder to drill in.
+ container/. Click the arrow on a folder row or double-click the row to drill in.
@@ -302,8 +356,8 @@ const FlowFilesPullDialogForm = ({ cachedFiles, flowId, onClose, onSuccess }: Fl
className="h-[360px]"
emptyState={emptyState}
enableSelection
- files={files}
- isLoading={isListingLoading && files.length === 0}
+ files={flatFiles}
+ isLoading={isListingLoading && flatFiles.length === 0}
onOpenDirectory={handleOpenDirectory}
onSelectionChange={setSelectedPaths}
/>
@@ -343,13 +397,7 @@ const FlowFilesPullDialogForm = ({ cachedFiles, flowId, onClose, onSuccess }: Fl
);
};
-export const FlowFilesPullDialog = ({
- cachedFiles,
- flowId,
- isOpen,
- onClose,
- onSuccess,
-}: FlowFilesPullDialogProps) => {
+export const FlowFilesPullDialog = ({ cachedFiles, flowId, isOpen, onClose, onSuccess }: FlowFilesPullDialogProps) => {
const handleDialogOpenChange = (nextOpen: boolean) => {
if (!nextOpen) {
onClose();
diff --git a/frontend/src/features/flows/files/flow-files.tsx b/frontend/src/features/flows/files/flow-files.tsx
index 19906f6c..cb7f6185 100644
--- a/frontend/src/features/flows/files/flow-files.tsx
+++ b/frontend/src/features/flows/files/flow-files.tsx
@@ -1,4 +1,4 @@
-import { ArrowDownToLine, FolderInput, FolderOutput, FolderUp, Info, Loader2, Search, X } from 'lucide-react';
+import { ArrowDownToLine, FolderInput, FolderOutput, FolderUp, Loader2, Search, Upload, X } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { toast } from 'sonner';
@@ -249,28 +249,6 @@ const FlowFiles = () => {
)}
/>
-
-
-
-
-
-
- Uploads are pushed to /work/uploads and are
- immediately accessible inside the container.
-
-
- Container files are snapshots pulled via Pull and stored
- separately.
-
+ Copied from the library to /work/resources — immediately accessible
+ inside the container.
+
+
@@ -319,10 +308,17 @@ const FlowFiles = () => {
-
- {isContainerRunning
- ? 'Pull file or directory from container'
- : 'Container is not running'}
+
+ {isContainerRunning ? (
+ <>
+
Pull file or directory from container
+
+ Snapshots are stored separately under Container.
+
+ >
+ ) : (
+
Container is not running
+ )}
diff --git a/frontend/src/pages/resources/resources.tsx b/frontend/src/pages/resources/resources.tsx
index 925995f6..a982002c 100644
--- a/frontend/src/pages/resources/resources.tsx
+++ b/frontend/src/pages/resources/resources.tsx
@@ -1,4 +1,4 @@
-import { Copy, FileSymlink, Folder, FolderPlus, FolderUp, Loader2, Search, X } from 'lucide-react';
+import { Copy, FileSymlink, Folder, FolderPlus, FolderUp, Loader2, Search, Upload, X } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { toast } from 'sonner';
@@ -49,84 +49,38 @@ const Resources = () => {
const [isMkdirOpen, setIsMkdirOpen] = useState(false);
// When the user invokes "New folder here" from a directory row's menu, we
- // need the dialog to seed itself with that *specific* directory rather
- // than the focus-derived `currentDir`. Cleared whenever the dialog closes
- // so the toolbar mkdir falls back to the focus default again.
+ // need the dialog to seed itself with that *specific* directory. Cleared
+ // whenever the dialog closes so the toolbar mkdir falls back to the
+ // library root again.
const [mkdirParentOverride, setMkdirParentOverride] = useState(null);
// Both dialogs accept an array now: a row-action click pushes a single-element
// array, the bulk bar pushes the full deduped selection. Empty / null array
// closes the dialog.
const [filesToMove, setFilesToMove] = useState(null);
const [filesToCopy, setFilesToCopy] = useState(null);
- // The path of the row currently focused in the FileManager (roving tabindex).
- // `null` until the user has actually clicked / Tab-ed into a row, so that we
- // can distinguish "user explicitly chose a directory" from the auto-fallback
- // the FileManager uses for its keyboard handler.
- const [activeRowPath, setActiveRowPath] = useState(null);
- // Index resources by virtual path so deriving `currentDir` (and other
- // path-based lookups) stays O(1) regardless of library size.
- const resourcesByPath = useMemo(() => {
- const map = new Map();
-
- for (const resource of resources) {
- map.set(resource.path, resource);
- }
-
- return map;
- }, [resources]);
-
- /**
- * Virtual directory the next upload / mkdir / drop should target. Resolved
- * from `activeRowPath`:
- * - directory row → the directory's own path (upload lands inside it)
- * - file row → the file's parent directory (sibling upload)
- * - no focused row → '' (library root)
- * - stale path → '' (focused row was deleted by a subscription)
- *
- * Mirrors the same resolution rule that the FileManager's internal
- * drag-and-drop uses (`resolveDropTargetDir` in `use-file-manager-dnd`),
- * so context-aware uploads land where users would expect a file dropped
- * onto the same row to land.
- */
- const currentDir = useMemo(() => {
- if (!activeRowPath) {
- return '';
- }
-
- const resource = resourcesByPath.get(activeRowPath);
-
- if (!resource) {
- return '';
- }
-
- if (resource.isDir) {
- return resource.path;
- }
-
- const idx = resource.path.lastIndexOf('/');
-
- return idx === -1 ? '' : resource.path.slice(0, idx);
- }, [activeRowPath, resourcesByPath]);
-
- const upload = useResourcesUpload({ defaultDir: currentDir });
+ // Toolbar / empty-area mkdir + upload always target the library root —
+ // row-level "Upload here" / "New folder here" handlers carry their own
+ // explicit path, and DnD passes the destination per drop, so these
+ // entry points don't need a focus-derived fallback directory.
+ const upload = useResourcesUpload();
const deletion = useResourcesDelete();
const { move } = useResourcesMove();
const canAcceptDrop = !upload.isUploading;
const { dragHandlers, isDragging } = useFilesDragAndDrop({
canAcceptDrop,
- // `upload.uploadFiles` is reference-stable: it reads the latest
- // `defaultDir` through a ref, so changing the focused row does not
- // invalidate the drag handlers below.
+ // Page-level drop falls back to the hook's defaults — i.e. the
+ // library root, since `useResourcesUpload` is invoked without a
+ // `defaultDir`.
onDrop: upload.uploadFiles,
});
// Per-row external file drop (OS desktop → folder row): forward the
// dropped files together with the resolved directory so the upload lands
- // exactly where the user released, not in the focus-derived `currentDir`.
- // FileManager already stops propagation on the row, so this never
- // double-fires alongside the page-level `dragHandlers` above.
+ // exactly where the user released, not in the library root that the
+ // page-level `dragHandlers` above default to. FileManager already stops
+ // propagation on the row, so this never double-fires.
const handleExternalFileDrop = useCallback(
async (droppedFiles: File[], destinationDir: string): Promise => {
await upload.uploadFiles(droppedFiles, { dir: destinationDir });
@@ -270,19 +224,19 @@ const Resources = () => {
{
appliesToDirs: true,
appliesToFiles: false,
- icon: FolderUp,
- id: 'resources-upload-here',
- label: 'Upload files here',
- onSelect: handleUploadHere,
+ icon: FolderPlus,
+ id: 'resources-mkdir-here',
+ label: 'New folder',
+ onSelect: handleMkdirHere,
separatorBefore: true,
},
{
appliesToDirs: true,
appliesToFiles: false,
- icon: FolderPlus,
- id: 'resources-mkdir-here',
- label: 'New folder here',
- onSelect: handleMkdirHere,
+ icon: Upload,
+ id: 'resources-upload-here',
+ label: 'Upload files',
+ onSelect: handleUploadHere,
},
{
appliesToDirs: true,
@@ -320,10 +274,7 @@ const Resources = () => {
// Right-click anywhere outside a row in the tree → mirror the toolbar
// gestures so users have a closer-to-pointer entry point. Both items
- // resolve through the same focus-derived `currentDir` as the toolbar
- // buttons (via `defaultDir` / `mkdirParentOverride === null`), so the
- // outcome — and the toolbar tooltip telling the user *where* it lands —
- // stays identical no matter which surface the user invokes.
+ // target the library root, identical to the toolbar buttons.
const fileManagerEmptyAreaActions = useMemo(
() => [
{
@@ -385,11 +336,6 @@ const Resources = () => {
/>
);
- // Human-readable target for the toolbar tooltips: matches the same wording
- // used in the upload success toast so users see the same "to /reports/2025"
- // / "to your library" phrasing both before and after the action.
- const uploadTargetLabel = currentDir ? `/${currentDir}` : 'library root';
-
const noMatchesState = (
@@ -408,7 +354,7 @@ const Resources = () => {
<>
{pageHeader}