657 lines
20 KiB
TypeScript
657 lines
20 KiB
TypeScript
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import {
|
|
TrashIcon,
|
|
AnalyzeIcon,
|
|
IconX,
|
|
FolderOutlineIcon,
|
|
TagIcon,
|
|
CorrespondentIcon,
|
|
LoaderIcon,
|
|
} from '../ui/icons';
|
|
import SelectionAssignmentMenu, { SelectionAssignmentMenuItem } from './SelectionAssignmentMenu';
|
|
import SelectionSummary from './SelectionSummary';
|
|
import { api, useAppState } from '../app/appState';
|
|
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
|
|
|
|
const ROOT_FOLDER_LABEL = 'Documents';
|
|
|
|
type DocumentId = string | number;
|
|
type NullableDocumentId = DocumentId | null | undefined;
|
|
|
|
type SelectedIdList = Array<NullableDocumentId> | null | undefined;
|
|
|
|
type FolderTreeNode = {
|
|
id?: DocumentId;
|
|
name?: string;
|
|
label?: string;
|
|
value?: DocumentId;
|
|
children?: FolderTreeNode[];
|
|
};
|
|
|
|
interface TagOption {
|
|
id?: DocumentId;
|
|
label?: string;
|
|
name?: string;
|
|
color?: string | null;
|
|
}
|
|
|
|
interface CorrespondentOption {
|
|
id?: DocumentId;
|
|
name?: string;
|
|
label?: string;
|
|
}
|
|
|
|
interface DocumentLike {
|
|
id?: DocumentId;
|
|
tags?: TagOption[];
|
|
correspondents?: CorrespondentOption[];
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface BulkTagMutationArgs {
|
|
label: string;
|
|
input: unknown;
|
|
documentIds: DocumentId[];
|
|
}
|
|
|
|
interface BulkCorrespondentAddArgs {
|
|
name: string;
|
|
input: unknown;
|
|
documentIds: DocumentId[];
|
|
}
|
|
|
|
interface BulkCorrespondentRemoveArgs {
|
|
assignments: Array<{ correspondent_id: DocumentId }>;
|
|
documentIds: DocumentId[];
|
|
}
|
|
|
|
export interface SelectionFloatingActionsProps {
|
|
selectionCount?: number;
|
|
selectedDocumentIds?: SelectedIdList;
|
|
selectedFolderIds?: SelectedIdList;
|
|
documentLookup?: Map<DocumentId, DocumentLike> | null;
|
|
tags?: TagOption[] | null;
|
|
tagLookupById?: Map<DocumentId, TagOption> | null;
|
|
correspondents?: CorrespondentOption[] | null;
|
|
folderOptions?: SelectionAssignmentMenuItem[] | null;
|
|
onBulkTagAdd?: (args: BulkTagMutationArgs) => Promise<void> | void;
|
|
onBulkTagRemove?: (args: BulkTagMutationArgs) => Promise<void> | void;
|
|
onBulkCorrespondentAdd?: (args: BulkCorrespondentAddArgs) => Promise<void> | void;
|
|
onBulkCorrespondentRemove?: (args: BulkCorrespondentRemoveArgs) => Promise<void> | void;
|
|
onBulkReanalyze?: (documentIds: DocumentId[]) => Promise<void> | void;
|
|
onDeleteSelection?: () => void;
|
|
onClearSelection?: () => void;
|
|
onMoveDocumentsToFolder?: (documentIds: DocumentId[], folderId: DocumentId) => Promise<void> | void;
|
|
}
|
|
|
|
const normalizeDocumentList = (selectedIds?: SelectedIdList): DocumentId[] =>
|
|
Array.isArray(selectedIds)
|
|
? selectedIds.filter((value): value is DocumentId => value !== null && value !== undefined)
|
|
: [];
|
|
|
|
const isRecord = (value: unknown): value is Record<string, unknown> => value != null && Object(value) === value;
|
|
|
|
const splitLabelSegments = (input: unknown): string[] => {
|
|
const text = `${input ?? ''}`.trim();
|
|
return text ? text.split('/') : [];
|
|
};
|
|
|
|
const buildFolderTreeOptions = (tree?: FolderTreeNode[] | null): SelectionAssignmentMenuItem[] => {
|
|
const entries: SelectionAssignmentMenuItem[] = [];
|
|
|
|
const traverse = (nodes: FolderTreeNode[] | undefined | null, parentSegments: string[]) => {
|
|
if (!Array.isArray(nodes) || nodes.length === 0) {
|
|
return;
|
|
}
|
|
nodes.forEach((node) => {
|
|
if (!node || !node.id) {
|
|
return;
|
|
}
|
|
const trimmedName = node.name?.trim?.();
|
|
const name = trimmedName?.length ? trimmedName : 'Folder';
|
|
const nextSegments = parentSegments.concat([name]);
|
|
const label = nextSegments.join('/');
|
|
entries.push({
|
|
id: node.id,
|
|
label,
|
|
state: 'none',
|
|
payload: {
|
|
id: node.id,
|
|
label,
|
|
segments: nextSegments,
|
|
depth: Math.max(nextSegments.length - 1, 0),
|
|
},
|
|
});
|
|
if (Array.isArray(node.children) && node.children.length) {
|
|
traverse(node.children, nextSegments);
|
|
}
|
|
});
|
|
};
|
|
|
|
traverse(Array.isArray(tree) ? tree : [], [ROOT_FOLDER_LABEL]);
|
|
|
|
entries.sort((a, b) => (a.label || '').localeCompare(b.label || '', undefined, { sensitivity: 'base' }));
|
|
|
|
return [{ id: 'root', label: ROOT_FOLDER_LABEL, state: 'none', payload: { id: 'root' } }, ...entries];
|
|
};
|
|
|
|
const buildTagAssignments = (
|
|
selectedDocuments: DocumentLike[],
|
|
tagLookupById: Map<DocumentId, TagOption> | null | undefined,
|
|
tags: TagOption[] | null | undefined,
|
|
total: number,
|
|
): SelectionAssignmentMenuItem[] => {
|
|
if (!total) {
|
|
return [];
|
|
}
|
|
|
|
const map = new Map<string | number, {
|
|
id?: DocumentId;
|
|
label: string;
|
|
color: string | null;
|
|
count: number;
|
|
total: number;
|
|
}>();
|
|
|
|
const ensureEntry = (id?: DocumentId, label?: string, color: string | null = null) => {
|
|
const key = id ?? label;
|
|
if (!key || !label) {
|
|
return null;
|
|
}
|
|
if (!map.has(key)) {
|
|
map.set(key, {
|
|
id,
|
|
label,
|
|
color,
|
|
count: 0,
|
|
total,
|
|
});
|
|
}
|
|
return map.get(key) ?? null;
|
|
};
|
|
|
|
selectedDocuments.forEach((doc) => {
|
|
(doc?.tags || []).forEach((tag) => {
|
|
const lookupColor = tag?.id && tagLookupById instanceof Map ? tagLookupById.get(tag.id)?.color : null;
|
|
const entry = ensureEntry(tag?.id, tag?.label, tag?.color ?? lookupColor ?? null);
|
|
if (entry) {
|
|
entry.count += 1;
|
|
}
|
|
});
|
|
});
|
|
|
|
(tags || []).forEach((tag) => {
|
|
const lookupColor = tag?.id && tagLookupById instanceof Map ? tagLookupById.get(tag.id)?.color : null;
|
|
ensureEntry(tag?.id, tag?.label, tag?.color ?? lookupColor ?? null);
|
|
});
|
|
|
|
return Array.from(map.values()).map((entry) => {
|
|
const count = entry.count || 0;
|
|
const state = count === total ? 'all' : count > 0 ? 'partial' : 'none';
|
|
return {
|
|
id: entry.id ?? entry.label,
|
|
label: entry.label,
|
|
color: entry.color ?? null,
|
|
count,
|
|
total,
|
|
state,
|
|
payload: entry,
|
|
};
|
|
});
|
|
};
|
|
|
|
const buildCorrespondentAssignments = (
|
|
selectedDocuments: DocumentLike[],
|
|
correspondents: CorrespondentOption[] | null | undefined,
|
|
total: number,
|
|
): SelectionAssignmentMenuItem[] => {
|
|
if (!total) {
|
|
return [];
|
|
}
|
|
|
|
const map = new Map<string | number, {
|
|
id?: DocumentId;
|
|
label: string;
|
|
count: number;
|
|
total: number;
|
|
}>();
|
|
|
|
const ensureEntry = (id?: DocumentId, name?: string) => {
|
|
const key = id ?? name;
|
|
if (!key || !name) {
|
|
return null;
|
|
}
|
|
if (!map.has(key)) {
|
|
map.set(key, {
|
|
id,
|
|
label: name,
|
|
count: 0,
|
|
total,
|
|
});
|
|
}
|
|
return map.get(key) ?? null;
|
|
};
|
|
|
|
selectedDocuments.forEach((doc) => {
|
|
(doc?.correspondents || []).forEach((entry) => {
|
|
const target = ensureEntry(entry?.id, entry?.name);
|
|
if (target) {
|
|
target.count += 1;
|
|
}
|
|
});
|
|
});
|
|
|
|
(correspondents || []).forEach((entry) => {
|
|
ensureEntry(entry?.id, entry?.name || entry?.label);
|
|
});
|
|
|
|
return Array.from(map.values()).map((entry) => {
|
|
const count = entry.count || 0;
|
|
const state = count === total ? 'all' : count > 0 ? 'partial' : 'none';
|
|
return {
|
|
id: entry.id ?? entry.label,
|
|
label: entry.label,
|
|
count,
|
|
total,
|
|
state,
|
|
payload: entry,
|
|
};
|
|
});
|
|
};
|
|
|
|
const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
|
|
selectionCount = 0,
|
|
selectedDocumentIds = [],
|
|
selectedFolderIds = [],
|
|
documentLookup,
|
|
tags = [],
|
|
tagLookupById,
|
|
correspondents = [],
|
|
folderOptions = [],
|
|
onBulkTagAdd,
|
|
onBulkTagRemove,
|
|
onBulkCorrespondentAdd,
|
|
onBulkCorrespondentRemove,
|
|
onBulkReanalyze,
|
|
onDeleteSelection,
|
|
onClearSelection = null,
|
|
onMoveDocumentsToFolder,
|
|
}) => {
|
|
const { token, tenant } = useAppState() as { token?: string; tenant?: { id?: DocumentId } | null };
|
|
const tenantId = tenant?.id ?? null;
|
|
|
|
const documentLookupMap = useMemo(() => (
|
|
documentLookup instanceof Map ? documentLookup : new Map<DocumentId, DocumentLike>()
|
|
), [documentLookup]);
|
|
const tagLookupMap = tagLookupById instanceof Map ? tagLookupById : null;
|
|
|
|
const [remoteFolderOptions, setRemoteFolderOptions] = useState<SelectionAssignmentMenuItem[] | null>(null);
|
|
const [loadingFolders, setLoadingFolders] = useState(false);
|
|
const folderTreeFetchRef = useRef<Promise<SelectionAssignmentMenuItem[]> | null>(null);
|
|
|
|
useEffect(() => {
|
|
setRemoteFolderOptions(null);
|
|
folderTreeFetchRef.current = null;
|
|
setLoadingFolders(false);
|
|
}, [tenantId, token]);
|
|
|
|
const requestFolderTree = useCallback(async (): Promise<SelectionAssignmentMenuItem[]> => {
|
|
if (!token) {
|
|
setRemoteFolderOptions([]);
|
|
return [];
|
|
}
|
|
|
|
if (Array.isArray(remoteFolderOptions)) {
|
|
return remoteFolderOptions;
|
|
}
|
|
|
|
if (folderTreeFetchRef.current) {
|
|
return folderTreeFetchRef.current;
|
|
}
|
|
|
|
const fetchPromise = (async () => {
|
|
setLoadingFolders(true);
|
|
try {
|
|
const { data } = await api.get('/folders/tree');
|
|
const options = buildFolderTreeOptions(data);
|
|
setRemoteFolderOptions(options);
|
|
return options;
|
|
} catch (error) {
|
|
console.warn('[selection] Failed to load folder tree', error);
|
|
setRemoteFolderOptions([]);
|
|
return [];
|
|
} finally {
|
|
setLoadingFolders(false);
|
|
folderTreeFetchRef.current = null;
|
|
}
|
|
})();
|
|
|
|
folderTreeFetchRef.current = fetchPromise;
|
|
return fetchPromise;
|
|
}, [remoteFolderOptions, token]);
|
|
|
|
const handleMoveMenuOpen = useCallback(() => {
|
|
requestFolderTree();
|
|
}, [requestFolderTree]);
|
|
|
|
const effectiveFolderOptions = useMemo<SelectionAssignmentMenuItem[]>(() => {
|
|
if (remoteFolderOptions !== null) {
|
|
return remoteFolderOptions;
|
|
}
|
|
return Array.isArray(folderOptions) ? folderOptions : [];
|
|
}, [remoteFolderOptions, folderOptions]);
|
|
|
|
const documentIdList = useMemo<DocumentId[]>(
|
|
() => normalizeDocumentList(selectedDocumentIds),
|
|
[selectedDocumentIds],
|
|
);
|
|
|
|
const folderIdList = useMemo<DocumentId[]>(
|
|
() => normalizeDocumentList(selectedFolderIds),
|
|
[selectedFolderIds],
|
|
);
|
|
|
|
const documentCount = documentIdList.length;
|
|
const folderCount = folderIdList.length;
|
|
const totalCount = Number.isFinite(selectionCount)
|
|
? Number(selectionCount)
|
|
: documentCount + folderCount;
|
|
|
|
const selectedDocuments = useMemo<DocumentLike[]>(() => {
|
|
if (!documentIdList.length || !(documentLookupMap instanceof Map)) {
|
|
return [];
|
|
}
|
|
return documentIdList
|
|
.map((id) => documentLookupMap.get(id))
|
|
.filter((doc): doc is DocumentLike => Boolean(doc));
|
|
}, [documentIdList, documentLookupMap]);
|
|
|
|
const selectedDocCount = selectedDocuments.length;
|
|
|
|
const moveAssignments = useMemo<SelectionAssignmentMenuItem[]>(() => {
|
|
if (!Array.isArray(effectiveFolderOptions)) {
|
|
return [];
|
|
}
|
|
return effectiveFolderOptions
|
|
.map<SelectionAssignmentMenuItem | null>((option) => {
|
|
const id = (option?.id ?? option?.payload?.id ?? option?.value) as DocumentId | undefined;
|
|
if (!id) {
|
|
return null;
|
|
}
|
|
const label = option?.label || option?.payload?.label || option?.name || String(id);
|
|
const segments = splitLabelSegments(label);
|
|
const depth = Math.max(segments.length - 1, 0);
|
|
return {
|
|
id,
|
|
label,
|
|
state: 'none',
|
|
payload: {
|
|
id,
|
|
label,
|
|
segments,
|
|
depth,
|
|
},
|
|
};
|
|
})
|
|
.filter((entry): entry is SelectionAssignmentMenuItem => Boolean(entry));
|
|
}, [effectiveFolderOptions]);
|
|
|
|
const tagAssignments = useMemo(
|
|
() => buildTagAssignments(selectedDocuments, tagLookupMap, tags, selectedDocCount),
|
|
[selectedDocuments, tagLookupMap, tags, selectedDocCount],
|
|
);
|
|
|
|
const correspondentAssignments = useMemo(
|
|
() => buildCorrespondentAssignments(selectedDocuments, correspondents, selectedDocCount),
|
|
[selectedDocuments, correspondents, selectedDocCount],
|
|
);
|
|
|
|
const renderFolderLabel = useCallback((item: SelectionAssignmentMenuItem) => {
|
|
const payload = (item?.payload as { segments?: string[]; depth?: number }) || {};
|
|
const segments = payload.segments || splitLabelSegments(item.label);
|
|
const depth = payload.depth ?? Math.max(segments.length - 1, 0);
|
|
const clampedDepth = Math.min(depth, 6);
|
|
const indentWidth = clampedDepth > 0 ? clampedDepth * 0.9 : 0;
|
|
const name = segments.length ? segments[segments.length - 1] : item?.label || 'Folder';
|
|
const parentPath = segments.length > 1 ? segments.slice(0, -1).join(' / ') : '';
|
|
|
|
return (
|
|
<>
|
|
{indentWidth ? (
|
|
<span
|
|
className="selection-assignment__indent"
|
|
style={{ width: `${indentWidth}rem` }}
|
|
aria-hidden="true"
|
|
/>
|
|
) : null}
|
|
<span className="selection-assignment__folder-label">
|
|
<span className="selection-assignment__folder-name">{name}</span>
|
|
{parentPath ? (
|
|
<span className="selection-assignment__folder-path">{parentPath}</span>
|
|
) : null}
|
|
</span>
|
|
</>
|
|
);
|
|
}, []);
|
|
|
|
const handleToggleTagAssignment = useCallback(
|
|
async (item: SelectionAssignmentMenuItem) => {
|
|
if (!selectedDocCount || !item) {
|
|
return;
|
|
}
|
|
if (item.state === 'all') {
|
|
await onBulkTagRemove?.({ label: item.label || '', input: null, documentIds: documentIdList });
|
|
} else {
|
|
await onBulkTagAdd?.({ label: item.label || '', input: null, documentIds: documentIdList });
|
|
}
|
|
},
|
|
[selectedDocCount, onBulkTagAdd, onBulkTagRemove, documentIdList],
|
|
);
|
|
|
|
const handleCreateTagAssignment = useCallback(
|
|
async (label: string) => {
|
|
if (!selectedDocCount || !label) {
|
|
return;
|
|
}
|
|
await onBulkTagAdd?.({ label, input: null, documentIds: documentIdList });
|
|
},
|
|
[selectedDocCount, onBulkTagAdd, documentIdList],
|
|
);
|
|
|
|
const handleToggleCorrespondentAssignment = useCallback(
|
|
async (item: SelectionAssignmentMenuItem) => {
|
|
if (!selectedDocCount || !item) {
|
|
return;
|
|
}
|
|
if (item.state === 'all') {
|
|
if (!item.id) {
|
|
return;
|
|
}
|
|
await onBulkCorrespondentRemove?.({
|
|
assignments: [{ correspondent_id: item.id }],
|
|
documentIds: documentIdList,
|
|
});
|
|
} else {
|
|
await onBulkCorrespondentAdd?.({ name: item.label || '', input: null, documentIds: documentIdList });
|
|
}
|
|
},
|
|
[selectedDocCount, onBulkCorrespondentAdd, onBulkCorrespondentRemove, documentIdList],
|
|
);
|
|
|
|
const handleCreateCorrespondentAssignment = useCallback(
|
|
async (name: string) => {
|
|
if (!selectedDocCount || !name) {
|
|
return;
|
|
}
|
|
await onBulkCorrespondentAdd?.({ name, input: null, documentIds: documentIdList });
|
|
},
|
|
[selectedDocCount, onBulkCorrespondentAdd, documentIdList],
|
|
);
|
|
|
|
const handleMoveSelectionToFolder = useCallback(
|
|
async (option: unknown) => {
|
|
if (!documentIdList.length || !onMoveDocumentsToFolder) {
|
|
return;
|
|
}
|
|
const candidate = option as { id?: DocumentId; value?: DocumentId } | DocumentId | null | undefined;
|
|
const value = isRecord(candidate)
|
|
? (candidate?.id ?? candidate?.value ?? null)
|
|
: candidate;
|
|
if (!value && value !== 0) {
|
|
return;
|
|
}
|
|
await onMoveDocumentsToFolder(documentIdList, value as DocumentId);
|
|
},
|
|
[documentIdList, onMoveDocumentsToFolder],
|
|
);
|
|
|
|
const summaryNode = totalCount > 0 ? (
|
|
<SelectionSummary
|
|
documentCount={documentCount}
|
|
folderCount={folderCount}
|
|
totalCount={totalCount}
|
|
/>
|
|
) : null;
|
|
|
|
const showPrimaryButtons = Boolean(onBulkReanalyze || onDeleteSelection || onClearSelection);
|
|
|
|
const moveMenu = onMoveDocumentsToFolder ? (
|
|
<SelectionAssignmentMenu
|
|
label="Move"
|
|
triggerContent={(
|
|
<span className="quick-add__chip-label" title="Move">
|
|
{loadingFolders ? (
|
|
<LoaderIcon className="icon-inline icon--spin" aria-hidden="true" />
|
|
) : (
|
|
<FolderOutlineIcon className="icon-inline" aria-hidden="true" />
|
|
)}
|
|
<span className="quick-add__chip-text" aria-hidden="true">Move</span>
|
|
</span>
|
|
)}
|
|
items={moveAssignments}
|
|
placeholder="Search folders…"
|
|
emptyMessage={loadingFolders ? 'Loading folders…' : 'No folders'}
|
|
onToggle={(item) => handleMoveSelectionToFolder(item?.payload || item)}
|
|
disabled={!documentCount || (loadingFolders && !moveAssignments.length)}
|
|
createLabel={null}
|
|
showStateIndicators={false}
|
|
showCounts={false}
|
|
onOpenMenu={handleMoveMenuOpen}
|
|
renderItemLabel={renderFolderLabel}
|
|
/>
|
|
) : null;
|
|
|
|
const primaryButtons = showPrimaryButtons ? (
|
|
<div className="panel-floating__buttons">
|
|
{onBulkReanalyze ? (
|
|
<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}
|
|
{onDeleteSelection ? (
|
|
<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}
|
|
{onClearSelection ? (
|
|
<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>
|
|
) : null;
|
|
|
|
return (
|
|
<>
|
|
{summaryNode ? (
|
|
<span className="panel-floating__label">{summaryNode}</span>
|
|
) : null}
|
|
<div className="panel-floating-actions panel-floating-actions--assignments">
|
|
{moveMenu}
|
|
<SelectionAssignmentMenu
|
|
label="Tags"
|
|
triggerContent={(
|
|
<span className="quick-add__chip-label" title="Tags">
|
|
<TagIcon className="icon-inline" aria-hidden="true" />
|
|
<span className="quick-add__chip-text" aria-hidden="true">Tags</span>
|
|
</span>
|
|
)}
|
|
items={tagAssignments}
|
|
placeholder="Search tags…"
|
|
emptyMessage="No tags"
|
|
createLabel="Create"
|
|
onToggle={handleToggleTagAssignment}
|
|
onCreate={handleCreateTagAssignment}
|
|
disabled={!documentCount}
|
|
/>
|
|
<SelectionAssignmentMenu
|
|
label="Correspondents"
|
|
triggerContent={(
|
|
<span className="quick-add__chip-label" title="Correspondents">
|
|
<CorrespondentIcon className="icon-inline" aria-hidden="true" />
|
|
<span className="quick-add__chip-text" aria-hidden="true">Correspondents</span>
|
|
</span>
|
|
)}
|
|
items={correspondentAssignments}
|
|
placeholder="Search correspondents…"
|
|
emptyMessage="No correspondents"
|
|
createLabel="Create"
|
|
onToggle={handleToggleCorrespondentAssignment}
|
|
onCreate={handleCreateCorrespondentAssignment}
|
|
disabled={!documentCount}
|
|
/>
|
|
</div>
|
|
{primaryButtons}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export type SelectionFloatingPanelProps = Omit<SelectionFloatingActionsProps, 'selectedDocumentIds' | 'selectedFolderIds' | 'selectionCount'>;
|
|
|
|
export const SelectionFloatingPanel: React.FC<SelectionFloatingPanelProps> = ({ onClearSelection, ...rest }) => {
|
|
const { selectedDocumentIds, selectedFolderIds, clearSelection } = useWorkspaceSelectionContext();
|
|
const documentIds = Array.isArray(selectedDocumentIds) ? selectedDocumentIds : [];
|
|
const folderIds = Array.isArray(selectedFolderIds) ? selectedFolderIds : [];
|
|
const selectionCount = documentIds.length + folderIds.length;
|
|
if (selectionCount === 0) {
|
|
return null;
|
|
}
|
|
const handleClear = onClearSelection || clearSelection;
|
|
return (
|
|
<div className="panel-floating-region" aria-live="polite" aria-atomic="true">
|
|
<div className="panel-floating">
|
|
<SelectionFloatingActions
|
|
selectionCount={selectionCount}
|
|
selectedDocumentIds={documentIds}
|
|
selectedFolderIds={folderIds}
|
|
onClearSelection={handleClear}
|
|
{...rest}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
|
|
export default SelectionFloatingActions;
|