Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions plugins/file-manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ browser runs on. This plugin adds the other machine — the one bb itself runs o
line.
- **Right-click a row** in the file manager → **Add to chat**. Several files
selected means several mentions, one per file.
- **On mobile**, tick one or more rows, then tap **Actions** in the selection
bar → **Add to chat**. The same drawer also contains download, organize,
rename, properties and delete actions for the selection.
- **+ → From File Manager…** opens a small browser over the composer, starting
in your start folder. Tick as many files as you need: a checkbox on every
row, `Shift`-click for the run between two of them, and *Select every file
Expand Down Expand Up @@ -208,6 +211,13 @@ download, drag and drop, and upload. Dragging over a collapsed folder for about
0.7 s springs it open so you can drop deeper; dropping onto a file row resolves
to the folder that row lives in.

On a compact layout or a coarse-pointer device, tick a row or tile to reveal a
persistent selection bar. **Actions** opens the selected-item menu as a bottom
drawer; **Clear selection** removes the ticks. Native row dragging is disabled
there so a long press cannot turn the item into a browser drag ghost. This also
covers landscape phones and tablets; desktop row dragging and right-click menus
are unchanged.

Three behaviors worth knowing:

- `Ctrl`/`Cmd`+`A` selects every **visible** row, including expanded children.
Expand Down
15 changes: 13 additions & 2 deletions plugins/file-manager/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,8 @@ components/FileTable.tsx FRONTEND header row, sorting, rubber-
components/FileRow.tsx FRONTEND one row: icon, name, size, mtime, drag source/target
components/FileGallery.tsx FRONTEND the gallery view: thumbnail grid over the same handlers (§8.9)
components/RowContextMenu.tsx FRONTEND right-click menu for a selection
components/SelectionActionBar.tsx FRONTEND compact/touch selected-item actions
components/selected-entry-actions.ts FRONTEND shared selected-action policy and ordering
components/BackgroundContextMenu.tsx FRONTEND right-click menu for empty space
components/ActivityTray.tsx FRONTEND upload progress + extract jobs, bottom-right
components/EmptyState.tsx FRONTEND empty dir / no search results / escapesRoot dir
Expand Down Expand Up @@ -1107,6 +1109,7 @@ preview, and extracting it is what the gesture is for. Downloading stays on
the row menu, where it is explicit.
| right click on a row | `RowContextMenu`; if the row is not selected, select it first |
| right click on empty space | `BackgroundContextMenu` |
| select a row on a compact viewport or coarse primary pointer | show `SelectionActionBar`; **Actions** opens the same selected-item operations in a responsive bottom drawer |
| click on a breadcrumb | navigate to that ancestor |
| column header click | toggle sort field / direction (persisted via `savePreferences`) |

Expand Down Expand Up @@ -1152,7 +1155,15 @@ when the event target is an `input`, `textarea` or `[contenteditable]`.

**Internal (row → folder)**

* Rows are `draggable`. `dragstart`:
* Rows are `draggable` only outside compact layouts and coarse primary-pointer
devices. Those touch-oriented surfaces disable native row/tile dragging so
long-press cannot enter browser drag mode; selected-item operations remain
available from `SelectionActionBar`. Pointer capability is independent of
viewport width, so landscape phones and tablets follow the touch path too.
* `RowContextMenu` and `SelectionActionBar` render the same action groups from
`selectedEntryActionModel`; visibility, enablement, order and callbacks are
not reimplemented per surface.
* On a draggable row, `dragstart`:
`dataTransfer.effectAllowed = "move"`,
`setData("application/x-bb-file-manager", JSON.stringify(selectedPaths))`,
plus a `text/plain` fallback of newline-joined paths. If the dragged row is
Expand Down Expand Up @@ -1769,7 +1780,7 @@ first line plus the `matchMedia` / `scrollIntoView` stubs in the setup file.
| `registration.test.tsx` | `app.navPanels[0]` matches `{ id: "file-manager", title: "File Manager", icon: "FolderOpen", path: "files" }`; `headerContent` and `experimental_sidebarAccessory` are functions |
| `panel.test.tsx` | renders rows from a stubbed `listDir`; hidden toggle re-issues `listDir` with `showHidden: true`; sorting by size reorders without an RPC; search filters client-side; `emitRealtime("fs", { paths:[cwd] })` triggers exactly one refetch; `setRealtimeConnectionState("connected")` refetches |
| `selection.test.tsx` | click / ctrl-click / shift-click / `Ctrl+A` / `Escape` produce the expected selections |
| `menus.test.tsx` | right-click on a file shows Download/Rename/Cut/Copy/Delete; Delete opens the confirm dialog when `confirmOnDelete`, calls `deleteEntries` when confirmed |
| `menus.test.tsx` | right-click on a file shows Download/Rename/Cut/Copy/Delete; Delete opens the confirm dialog when `confirmOnDelete`, calls `deleteEntries` when confirmed; compact and wide coarse-pointer selection disable native row/tile dragging and expose the responsive drawer; desktop remains draggable; desktop and touch action IDs and disabled states stay in parity |
| `uploads.test.tsx` | dropping two `File`s calls `uploadCreate` twice and posts chunks in order (stub `XMLHttpRequest`); a 409 response resumes from `expected`; the tray shows percentages |
| `bookmarks.test.tsx` | §8.11: the star lights up for a bookmarked folder and toggles `addBookmark` / `removeBookmark`; the list navigates through `navigateTo`; a missing row is marked and removes itself; the rename dialog sends `renameBookmark`; both context menus toggle; the compact chrome keeps the star and moves the list into the overflow; the 51st is refused client-side |

Expand Down
6 changes: 5 additions & 1 deletion plugins/file-manager/components/FileGallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface FileGalleryProps {
selectedPaths: ReadonlySet<string>;
focusedPath: string | null;
cutPaths: ReadonlySet<string>;
dragEnabled: boolean;
/** Path currently highlighted as a drop target (a tile, or `..`). */
dropTargetPath: string | null;
/**
Expand Down Expand Up @@ -66,6 +67,7 @@ export interface FileGalleryProps {

interface GalleryTileProps {
entry: FileEntry;
dragEnabled: boolean;
selected: boolean;
focused: boolean;
cut: boolean;
Expand All @@ -85,6 +87,7 @@ interface GalleryTileProps {

function GalleryTileImpl({
entry,
dragEnabled,
selected,
focused,
cut,
Expand Down Expand Up @@ -119,7 +122,7 @@ function GalleryTileImpl({
data-selected={selected ? "true" : undefined}
data-drop-target={dropTarget ? "true" : undefined}
tabIndex={-1}
draggable
draggable={dragEnabled}
title={entry.name}
className={cn(
"group flex min-w-0 cursor-default flex-col gap-1 rounded-md p-1.5 select-none",
Expand Down Expand Up @@ -278,6 +281,7 @@ export function FileGallery(props: FileGalleryProps) {
<GalleryTile
key={entry.path}
entry={entry}
dragEnabled={props.dragEnabled}
selected={selectedPaths.has(entry.path)}
focused={focusedPath === entry.path}
cut={cutPaths.has(entry.path)}
Expand Down
100 changes: 61 additions & 39 deletions plugins/file-manager/components/FileManagerPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ import { effectiveKind, isFileEntry } from "./FileRow";
import { FileGallery } from "./FileGallery";
import { FileTable } from "./FileTable";
import { RowContextMenu } from "./RowContextMenu";
import { SelectionActionBar } from "./SelectionActionBar";
import type { SelectedEntryActionsProps } from "./selected-entry-actions";
import { Toolbar } from "./Toolbar";
import { BookmarkNameDialog } from "./dialogs/BookmarkNameDialog";
import { ConfirmDeleteDialog } from "./dialogs/ConfirmDeleteDialog";
Expand All @@ -121,6 +123,8 @@ import { NewFolderDialog } from "./dialogs/NewFolderDialog";
import { PropertiesDialog, type PropertiesTarget } from "./dialogs/PropertiesDialog";
import { RenameDialog } from "./dialogs/RenameDialog";
import { ContextMenu, ContextMenuTrigger } from "./ui/context-menu";
import { useIsCompactViewport } from "./ui/hooks/use-compact-viewport";
import { useIsCoarsePointer } from "./ui/hooks/use-coarse-pointer";
import {
publishPanelSnapshot,
resetPanelSnapshot,
Expand Down Expand Up @@ -385,6 +389,9 @@ export function FileManagerSurface({
threadId = null,
}: FileManagerSurfaceProps) {
const rpc = useFmRpc();
const isCompactViewport = useIsCompactViewport();
const isCoarsePointer = useIsCoarsePointer();
const touchActionsEnabled = isCompactViewport || isCoarsePointer;
const subPath = location.subPath;
const locationRef = useRef(location);
locationRef.current = location;
Expand Down Expand Up @@ -2236,6 +2243,50 @@ export function FileManagerSurface({
const rowMenuEntry = menuEntries.length === 1 ? menuEntries[0] : undefined;
const rowMenuBookmarked =
rowMenuEntry !== undefined && bookmarks.isBookmarked(rowMenuEntry.path);
const selectedActionEntry = selectedEntries.length === 1 ? selectedEntries[0] : undefined;
const selectedActionBookmarked =
selectedActionEntry !== undefined && bookmarks.isBookmarked(selectedActionEntry.path);
const actionPropsFor = (
entries: readonly FileEntry[],
bookmarked: boolean,
): SelectedEntryActionsProps => ({
entries,
writable,
canPaste,
canExtract:
entries.length === 1 &&
entries[0]?.archiveFormat != null &&
isFormatSupported(entries[0].archiveFormat, archiveSupport),
onOpen: openEntry,
onDownload: () => downloadSelection(entries),
onAddToChat: () => addToChat(entries),
onExtract: (entry) => setDialog({ kind: "extract", entry }),
onCut: () => clipboard.cut(topLevelPaths(entries.map((entry) => entry.path))),
onCopy: () => clipboard.copy(topLevelPaths(entries.map((entry) => entry.path))),
onPaste: paste,
onMoveTo: () =>
setDialog({
kind: "picker",
mode: "move",
paths: entries.map((entry) => entry.path),
}),
onCopyTo: () =>
setDialog({
kind: "picker",
mode: "copy",
paths: entries.map((entry) => entry.path),
}),
onRename: (entry) => setDialog({ kind: "rename", entry }),
onCopyPath: () => copyPathsToClipboard(entries.map((entry) => entry.path)),
onDelete: () => requestDelete(entries),
onSetStartFolder: (entry) => setStartFolder(entry.path),
onProperties: () => openProperties(entries),
bookmarked,
canToggleBookmark: !bookmarks.loading,
onToggleBookmark: (entry) => toggleBookmark(entry.path),
});
const selectedActionProps = actionPropsFor(selectedEntries, selectedActionBookmarked);
const rowMenuActionProps = actionPropsFor(menuEntries, rowMenuBookmarked);
const bookmarkItems = {
bookmarks: bookmarks.bookmarks,
currentBookmarked,
Expand Down Expand Up @@ -2345,6 +2396,13 @@ export function FileManagerSurface({
pathFocusTick={pathFocusTick}
/>

{touchActionsEnabled && selectedEntries.length > 0 ? (
<SelectionActionBar
{...selectedActionProps}
onClear={selection.clear}
/>
) : null}

{stateError === null ? null : (
<ErrorBanner
error={stateError}
Expand Down Expand Up @@ -2405,6 +2463,7 @@ export function FileManagerSurface({
selectedPaths={selection.selected}
focusedPath={selection.focus}
cutPaths={cutPaths}
dragEnabled={!touchActionsEnabled}
dropTargetPath={dropTarget}
previewBaseUrl={previewBaseUrl}
parentPath={parentPath}
Expand All @@ -2431,6 +2490,7 @@ export function FileManagerSurface({
selectedPaths={selection.selected}
focusedPath={selection.focus}
cutPaths={cutPaths}
dragEnabled={!touchActionsEnabled}
sortField={sortField}
sortDirection={sortDirection}
onSort={handleHeaderSort}
Expand Down Expand Up @@ -2463,45 +2523,7 @@ export function FileManagerSurface({
</ContextMenuTrigger>

{menuEntries.length > 0 ? (
<RowContextMenu
entries={menuEntries}
writable={writable}
canPaste={canPaste}
canExtract={
menuEntries.length === 1 &&
menuEntries[0]?.archiveFormat != null &&
isFormatSupported(menuEntries[0].archiveFormat, archiveSupport)
}
onOpen={openEntry}
onDownload={() => downloadSelection(menuEntries)}
onAddToChat={() => addToChat(menuEntries)}
onExtract={(entry) => setDialog({ kind: "extract", entry })}
onCut={() => clipboard.cut(topLevelPaths(menuEntries.map((entry) => entry.path)))}
onCopy={() => clipboard.copy(topLevelPaths(menuEntries.map((entry) => entry.path)))}
onPaste={paste}
onMoveTo={() =>
setDialog({
kind: "picker",
mode: "move",
paths: menuEntries.map((entry) => entry.path),
})
}
onCopyTo={() =>
setDialog({
kind: "picker",
mode: "copy",
paths: menuEntries.map((entry) => entry.path),
})
}
onRename={(entry) => setDialog({ kind: "rename", entry })}
onCopyPath={() => copyPathsToClipboard(menuEntries.map((entry) => entry.path))}
onDelete={() => requestDelete(menuEntries)}
onSetStartFolder={(entry) => setStartFolder(entry.path)}
onProperties={() => openProperties(menuEntries)}
bookmarked={rowMenuBookmarked}
canToggleBookmark={!bookmarks.loading}
onToggleBookmark={(entry) => toggleBookmark(entry.path)}
/>
<RowContextMenu {...rowMenuActionProps} />
) : (
<BackgroundContextMenu
writable={writable}
Expand Down
5 changes: 4 additions & 1 deletion plugins/file-manager/components/FileRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export function rowDomId(path: string): string {

export interface FileRowProps {
entry: FileEntry;
/** Native file dragging is disabled on compact touch-oriented layouts. */
dragEnabled: boolean;
/** 0 for a row of the current directory; +1 per expanded ancestor. */
depth: number;
/** A navigable directory shallower than MAX_TREE_DEPTH gets a chevron. */
Expand Down Expand Up @@ -90,6 +92,7 @@ export interface FileRowProps {

function FileRowImpl({
entry,
dragEnabled,
depth,
expandable,
expanded,
Expand Down Expand Up @@ -136,7 +139,7 @@ function FileRowImpl({
data-selected={selected ? "true" : undefined}
data-drop-target={dropTarget ? "true" : undefined}
tabIndex={-1}
draggable
draggable={dragEnabled}
title={title}
className={cn(
"h-9 border-b border-border-hairline text-sm select-none",
Expand Down
2 changes: 2 additions & 0 deletions plugins/file-manager/components/FileTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface FileTableProps {
selectedPaths: ReadonlySet<string>;
focusedPath: string | null;
cutPaths: ReadonlySet<string>;
dragEnabled: boolean;
sortField: SortField;
sortDirection: SortDirection;
onSort: (field: SortField) => void;
Expand Down Expand Up @@ -313,6 +314,7 @@ export function FileTable(props: FileTableProps) {
<FileRow
key={row.key}
entry={row.entry}
dragEnabled={props.dragEnabled}
depth={row.depth}
expandable={row.expandable}
expanded={row.expanded}
Expand Down
Loading