floating selection box

This commit is contained in:
2025-11-03 23:10:03 +01:00
parent 8d4adbea42
commit 9ad704f50a
6 changed files with 164 additions and 141 deletions
+5 -43
View File
@@ -6,8 +6,6 @@ import {
RefreshIcon,
MinusVerticalIcon,
InfoIcon,
FileIcon,
FolderOutlineIcon,
} from '../ui/icons';
import BreadcrumbTrail from '../ui/BreadcrumbTrail';
import createWorkspaceSurfaceConfig from './workspaceHeader';
@@ -609,6 +607,7 @@ export const createDocumentsSurface = ({
selectedDocumentIds,
selectedFolderIds,
onDeleteSelection,
onClearSelection,
tags,
correspondents,
documentLookup,
@@ -629,45 +628,6 @@ export const createDocumentsSurface = ({
const documentSelectionCount = Array.isArray(selectedDocumentIds) ? selectedDocumentIds.length : 0;
const folderSelectionCount = Array.isArray(selectedFolderIds) ? selectedFolderIds.length : 0;
const selectionCount = documentSelectionCount + folderSelectionCount;
let selectionLabel = null;
if (selectionCount) {
const tokens = [];
if (documentSelectionCount) {
tokens.push({
key: 'documents',
count: documentSelectionCount,
icon: <FileIcon className="selection-summary__icon" size={16} />,
});
}
if (folderSelectionCount) {
tokens.push({
key: 'folders',
count: folderSelectionCount,
icon: <FolderOutlineIcon className="selection-summary__icon" size={16} />,
});
}
if (tokens.length) {
selectionLabel = (
<span className="selection-summary">
{tokens.map((token, index) => (
<React.Fragment key={token.key}>
{index > 0 ? <span className="selection-summary__separator">·</span> : null}
<span className="selection-summary__token">
<span className="selection-summary__count">{token.count}</span>
<span className="selection-summary__multiply">×</span>
{token.icon}
</span>
</React.Fragment>
))}
</span>
);
} else {
selectionLabel = `${selectionCount} item${selectionCount === 1 ? '' : 's'}`;
}
}
const actions = createDocumentsTableHeaderActions({
viewMode,
@@ -675,11 +635,12 @@ export const createDocumentsSurface = ({
onRefresh,
});
const floatingActions = selectionLabel
const floatingActions = selectionCount > 0
? (
<SelectionFloatingActions
selectionCount={selectionCount}
selectedDocumentIds={selectedDocumentIds}
selectedFolderIds={selectedFolderIds}
documentLookup={documentLookup}
tags={tags}
tagLookupById={tagLookupById}
@@ -690,6 +651,7 @@ export const createDocumentsSurface = ({
onBulkCorrespondentRemove={onBulkCorrespondentRemove}
onBulkReanalyze={onBulkReanalyze}
onDeleteSelection={onDeleteSelection}
onClearSelection={onClearSelection}
/>
)
: null;
@@ -708,7 +670,7 @@ export const createDocumentsSurface = ({
onNavigateParent,
actions,
breadcrumbs,
selectionLabel,
selectionLabel: null,
floatingActions,
content: <DocumentsPanel {...tableProps} showHeader={false} />,
detail,
@@ -1,6 +1,7 @@
import React, { useCallback, useMemo } from 'react';
import { TrashIcon, AnalyzeIcon } from '../ui/icons';
import { TrashIcon, AnalyzeIcon, IconX } from '../ui/icons';
import SelectionAssignmentMenu from './SelectionAssignmentMenu';
import SelectionSummary from './SelectionSummary';
const normalizeDocumentList = (selectedDocumentIds) =>
Array.isArray(selectedDocumentIds) ? selectedDocumentIds.filter(Boolean) : [];
@@ -112,6 +113,7 @@ const buildCorrespondentAssignments = (selectedDocuments, correspondents, total)
const SelectionFloatingActions = ({
selectionCount = 0,
selectedDocumentIds,
selectedFolderIds = [],
documentLookup,
tags,
tagLookupById,
@@ -122,12 +124,24 @@ const SelectionFloatingActions = ({
onBulkCorrespondentRemove,
onBulkReanalyze,
onDeleteSelection,
onClearSelection = null,
}) => {
const documentIdList = useMemo(
() => normalizeDocumentList(selectedDocumentIds),
[selectedDocumentIds],
);
const folderIdList = useMemo(
() => normalizeDocumentList(selectedFolderIds),
[selectedFolderIds],
);
const documentCount = documentIdList.length;
const folderCount = folderIdList.length;
const totalCount = typeof selectionCount === 'number'
? selectionCount
: documentCount + folderCount;
const selectedDocuments = useMemo(() => {
if (!documentIdList.length || !(documentLookup instanceof Map)) {
return [];
@@ -203,52 +217,77 @@ const SelectionFloatingActions = ({
[selectedDocCount, onBulkCorrespondentAdd, documentIdList],
);
const summaryNode = totalCount > 0 ? (
<SelectionSummary
documentCount={documentCount}
folderCount={folderCount}
totalCount={totalCount}
/>
) : null;
return (
<div className="panel-floating-actions">
<SelectionAssignmentMenu
label="Tags"
items={tagAssignments}
placeholder="Search tags…"
emptyMessage="No tags"
createLabel="Create"
onToggle={handleToggleTagAssignment}
onCreate={handleCreateTagAssignment}
disabled={!selectedDocCount}
/>
<SelectionAssignmentMenu
label="Correspondents"
items={correspondentAssignments}
placeholder="Search correspondents…"
emptyMessage="No correspondents"
createLabel="Create"
onToggle={handleToggleCorrespondentAssignment}
onCreate={handleCreateCorrespondentAssignment}
disabled={!selectedDocCount}
/>
{typeof onBulkReanalyze === 'function' ? (
<button
type="button"
className="icon-button panel-floating-actions__button"
onClick={() => onBulkReanalyze(documentIdList)}
aria-label="Re-run analysis for selection"
title="Re-run analysis for selection"
disabled={!documentIdList.length}
>
<AnalyzeIcon className="icon-inline" />
</button>
<>
{summaryNode ? (
<span className="panel-floating__label">{summaryNode}</span>
) : null}
{typeof onDeleteSelection === 'function' ? (
<button
type="button"
className="icon-button danger panel-floating-actions__button"
onClick={onDeleteSelection}
aria-label="Delete selected items"
disabled={selectionCount === 0}
>
<TrashIcon className="icon-inline" />
</button>
) : null}
</div>
<div className="panel-floating-actions">
<SelectionAssignmentMenu
label="Tags"
items={tagAssignments}
placeholder="Search tags…"
emptyMessage="No tags"
createLabel="Create"
onToggle={handleToggleTagAssignment}
onCreate={handleCreateTagAssignment}
disabled={!documentCount}
/>
<SelectionAssignmentMenu
label="Correspondents"
items={correspondentAssignments}
placeholder="Search correspondents…"
emptyMessage="No correspondents"
createLabel="Create"
onToggle={handleToggleCorrespondentAssignment}
onCreate={handleCreateCorrespondentAssignment}
disabled={!documentCount}
/>
{typeof onBulkReanalyze === 'function' ? (
<button
type="button"
className="icon-button panel-floating-actions__button"
onClick={() => onBulkReanalyze(documentIdList)}
aria-label="Re-run analysis for selection"
title="Re-run analysis for selection"
disabled={documentIdList.length === 0}
>
<AnalyzeIcon className="icon-inline" />
</button>
) : null}
{typeof onDeleteSelection === 'function' ? (
<button
type="button"
className="icon-button danger panel-floating-actions__button"
onClick={onDeleteSelection}
aria-label="Delete selected items"
disabled={totalCount === 0}
>
<TrashIcon className="icon-inline" />
</button>
) : null}
{typeof onClearSelection === 'function' ? (
<button
type="button"
className="icon-button panel-floating-actions__button"
onClick={onClearSelection}
aria-label="Clear selection"
title="Clear selection"
disabled={totalCount === 0}
>
<IconX className="icon-inline" />
</button>
) : null}
</div>
</>
);
};
@@ -0,0 +1,56 @@
import React from 'react';
import { FileIcon, FolderOutlineIcon } from '../ui/icons';
const SelectionSummary = ({ documentCount = 0, folderCount = 0, totalCount = 0 }) => {
const docCount = Number(documentCount) || 0;
const folderCountNumber = Number(folderCount) || 0;
const aggregateCount = docCount + folderCountNumber;
const resolvedTotal = Number(totalCount) || aggregateCount;
if (!docCount && !folderCountNumber && !resolvedTotal) {
return null;
}
const tokens = [];
if (docCount) {
tokens.push({
key: 'documents',
count: docCount,
icon: <FileIcon className="selection-summary__icon" size={16} />,
});
}
if (folderCountNumber) {
tokens.push({
key: 'folders',
count: folderCountNumber,
icon: <FolderOutlineIcon className="selection-summary__icon" size={16} />,
});
}
if (!tokens.length) {
const count = resolvedTotal;
return (
<span className="selection-summary selection-summary--text">
{`${count} item${count === 1 ? '' : 's'}`}
</span>
);
}
return (
<span className="selection-summary">
{tokens.map((token, index) => (
<React.Fragment key={token.key}>
{index > 0 ? <span className="selection-summary__separator">·</span> : null}
<span className="selection-summary__token">
<span className="selection-summary__count">{token.count}</span>
{token.icon}
</span>
</React.Fragment>
))}
</span>
);
};
export default SelectionSummary;