typescript
This commit is contained in:
@@ -3,7 +3,7 @@ import type { ReactNode } from 'react';
|
||||
import DocumentSummarySection, { DocumentSummarySectionProps } from './DocumentSummarySection';
|
||||
import { buildDocumentMetadataItems, extractDocumentMetadataPayload } from './documentMetadata';
|
||||
|
||||
type PanelTab = { id: string; label: string; render: () => ReactNode };
|
||||
type PanelTab = { id: string; label: string; render: (context?: Record<string, unknown>) => ReactNode };
|
||||
|
||||
type ContentState =
|
||||
| { status: 'idle'; data: null; error: null }
|
||||
@@ -13,7 +13,7 @@ type ContentState =
|
||||
| { status: 'unavailable'; data: null; error: null }
|
||||
| { status: 'error'; data: null; error: unknown };
|
||||
|
||||
interface DocumentInfoPanelProps {
|
||||
export interface DocumentInfoPanelProps {
|
||||
document: DocumentSummarySectionProps['document'];
|
||||
summaryProps?: Omit<DocumentSummarySectionProps, 'document' | 'detailItems' | 'layout'>;
|
||||
metadataItems?: Array<{ label: string; value?: string }>;
|
||||
|
||||
@@ -110,6 +110,31 @@ interface QuickAddEntry {
|
||||
original: QuickAddOption | string;
|
||||
}
|
||||
|
||||
const resolveOptionName = (source: unknown): string => {
|
||||
if (!source) {
|
||||
return '';
|
||||
}
|
||||
if (typeof source === 'string') {
|
||||
return source.trim();
|
||||
}
|
||||
if (typeof source === 'object') {
|
||||
const candidate = source as { name?: string; label?: string; trim?: () => string };
|
||||
if (typeof candidate.name === 'string' && candidate.name.trim()) {
|
||||
return candidate.name.trim();
|
||||
}
|
||||
if (typeof candidate.label === 'string' && candidate.label.trim()) {
|
||||
return candidate.label.trim();
|
||||
}
|
||||
if (typeof candidate.trim === 'function') {
|
||||
const viaTrim = candidate.trim();
|
||||
if (typeof viaTrim === 'string' && viaTrim.trim()) {
|
||||
return viaTrim.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const normalizeQuickAddOption = (option: QuickAddOption | string | null | undefined): QuickAddEntry | null => {
|
||||
if (option == null) {
|
||||
return null;
|
||||
@@ -348,16 +373,12 @@ export const CorrespondentSection: React.FC<CorrespondentSectionProps> = ({
|
||||
return;
|
||||
}
|
||||
const source = item.payload ?? item;
|
||||
const resolvedName =
|
||||
source?.name?.trim?.()
|
||||
|| source?.label?.trim?.()
|
||||
|| source?.trim?.()
|
||||
|| '';
|
||||
const resolvedName = resolveOptionName(source);
|
||||
if (!resolvedName) {
|
||||
return;
|
||||
}
|
||||
const payload = typeof source === 'object'
|
||||
? { ...source, name: resolvedName }
|
||||
const payload = (source && typeof source === 'object')
|
||||
? { ...(source as Record<string, unknown>), name: resolvedName }
|
||||
: { id: null, name: resolvedName };
|
||||
onAdd({ name: resolvedName, option: payload, input: null });
|
||||
},
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { CSSProperties, JSX, MutableRefObject } from 'react';
|
||||
import { getAssetFromVersion, resolveDocumentAssetUrl, createAssetView } from '../asset_manager';
|
||||
import {
|
||||
getAssetFromVersion,
|
||||
resolveDocumentAssetUrl,
|
||||
createAssetView,
|
||||
} from '../asset_manager';
|
||||
import type {
|
||||
DocumentLike as AssetManagerDocumentLike,
|
||||
AssetLike as AssetManagerAssetLike,
|
||||
EnsureAssetUrl as AssetManagerEnsureAssetUrl,
|
||||
GetAsset as AssetManagerGetAsset,
|
||||
} from '../asset_manager';
|
||||
|
||||
const DEFAULT_THUMBNAIL_SIZE = 48;
|
||||
|
||||
@@ -74,26 +84,10 @@ interface DocumentVersionLike {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface DocumentLike {
|
||||
id?: Identifier;
|
||||
current_version?: DocumentVersionLike;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface AssetLike {
|
||||
id?: Identifier;
|
||||
url?: string | null;
|
||||
metadata?: Record<string, unknown> | null;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
type EnsureAssetUrl = (
|
||||
documentId: Identifier,
|
||||
asset: AssetLike,
|
||||
options?: { start?: number; limit?: number; [key: string]: unknown },
|
||||
) => Promise<unknown> | void;
|
||||
|
||||
type GetDocumentAsset = (document: DocumentLike | null | undefined, assetType: string) => AssetLike | null | undefined;
|
||||
type DocumentLike = AssetManagerDocumentLike;
|
||||
type AssetLike = AssetManagerAssetLike;
|
||||
type EnsureAssetUrl = AssetManagerEnsureAssetUrl;
|
||||
type GetDocumentAsset = AssetManagerGetAsset;
|
||||
|
||||
interface DocumentThumbnailImageProps {
|
||||
document?: DocumentLike | null;
|
||||
|
||||
@@ -127,9 +127,9 @@ const DocumentsGrid: React.FC<DocumentsGridProps> = ({
|
||||
submitEditing: submitDocumentEditing,
|
||||
savingId: savingDocumentId,
|
||||
attachInputRef: attachDocumentInputRef,
|
||||
} = useInlineRename(onDocumentRename, {
|
||||
getCurrentValue: (doc) => doc?.title ?? '',
|
||||
getEntityId: (doc) => doc?.id ?? null,
|
||||
} = useInlineRename<DocumentLike>(onDocumentRename, {
|
||||
getCurrentValue: (doc: DocumentLike) => doc?.title ?? '',
|
||||
getEntityId: (doc: DocumentLike) => doc?.id ?? null,
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -141,9 +141,9 @@ const DocumentsGrid: React.FC<DocumentsGridProps> = ({
|
||||
submitEditing: submitFolderEditing,
|
||||
savingId: savingFolderId,
|
||||
attachInputRef: attachFolderInputRef,
|
||||
} = useInlineRename(onFolderRename, {
|
||||
getCurrentValue: (folder) => folder?.name ?? '',
|
||||
getEntityId: (folder) => folder?.id ?? null,
|
||||
} = useInlineRename<FolderLike>(onFolderRename, {
|
||||
getCurrentValue: (folder: FolderLike) => folder?.name ?? '',
|
||||
getEntityId: (folder: FolderLike) => folder?.id ?? null,
|
||||
});
|
||||
|
||||
const documentSelectionCount = selectedDocumentIdsSet?.size ?? 0;
|
||||
|
||||
@@ -132,9 +132,9 @@ const DocumentsList: React.FC<DocumentsListProps> = ({
|
||||
submitEditing: submitDocumentEditing,
|
||||
savingId: savingDocumentId,
|
||||
attachInputRef: attachDocumentInputRef,
|
||||
} = useInlineRename(onDocumentRename, {
|
||||
getCurrentValue: (doc) => doc?.title ?? '',
|
||||
getEntityId: (doc) => doc?.id ?? null,
|
||||
} = useInlineRename<DocumentLike>(onDocumentRename, {
|
||||
getCurrentValue: (doc: DocumentLike) => doc?.title ?? '',
|
||||
getEntityId: (doc: DocumentLike) => doc?.id ?? null,
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -146,9 +146,9 @@ const DocumentsList: React.FC<DocumentsListProps> = ({
|
||||
submitEditing: submitFolderEditing,
|
||||
savingId: savingFolderId,
|
||||
attachInputRef: attachFolderInputRef,
|
||||
} = useInlineRename(onFolderRename, {
|
||||
getCurrentValue: (folder) => folder?.name ?? '',
|
||||
getEntityId: (folder) => folder?.id ?? null,
|
||||
} = useInlineRename<FolderLike>(onFolderRename, {
|
||||
getCurrentValue: (folder: FolderLike) => folder?.name ?? '',
|
||||
getEntityId: (folder: FolderLike) => folder?.id ?? null,
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -503,38 +503,82 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const showPrimaryButtons = Boolean(onBulkReanalyze || onDeleteSelection || onClearSelection);
|
||||
|
||||
const moveMenu = typeof onMoveDocumentsToFolder === 'function' ? (
|
||||
<SelectionAssignmentMenu
|
||||
label="Move"
|
||||
triggerContent={(
|
||||
<span className="quick-add__chip-label">
|
||||
{loadingFolders ? (
|
||||
<LoaderIcon className="icon-inline icon--spin" aria-hidden="true" />
|
||||
) : (
|
||||
<FolderOutlineIcon className="icon-inline" aria-hidden="true" />
|
||||
)}
|
||||
{' '}
|
||||
Move
|
||||
</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">
|
||||
{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>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{summaryNode ? (
|
||||
<span className="panel-floating__label">{summaryNode}</span>
|
||||
) : null}
|
||||
<div className="panel-floating-actions">
|
||||
{typeof onMoveDocumentsToFolder === 'function' ? (
|
||||
<SelectionAssignmentMenu
|
||||
label="Move"
|
||||
triggerContent={(
|
||||
<span className="quick-add__chip-label">
|
||||
{loadingFolders ? (
|
||||
<LoaderIcon className="icon-inline icon--spin" aria-hidden="true" />
|
||||
) : (
|
||||
<FolderOutlineIcon className="icon-inline" aria-hidden="true" />
|
||||
)}
|
||||
{' '}
|
||||
Move
|
||||
</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}
|
||||
<div className="panel-floating-actions panel-floating-actions--assignments">
|
||||
{moveMenu}
|
||||
<SelectionAssignmentMenu
|
||||
label="Tags"
|
||||
triggerContent={(
|
||||
@@ -565,42 +609,8 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
|
||||
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>
|
||||
{primaryButtons}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import { openOcrTextInNewTab } from '../utils/ocr';
|
||||
import type {
|
||||
EnsureAssetUrl,
|
||||
EnsurePreviewData,
|
||||
GetDocumentAsset,
|
||||
DocumentLike as OcrDocumentLike,
|
||||
} from '../utils/ocr';
|
||||
|
||||
type ResolveApiPath = (path: string) => string;
|
||||
type EnsurePreviewData = (id: string | number) => Promise<void>;
|
||||
type EnsureAssetUrl = (id: string | number, asset: unknown, options?: unknown) => Promise<unknown>;
|
||||
type GetDocumentAsset = (document: DocumentLike, type: string) => unknown;
|
||||
|
||||
interface DocumentVersion {
|
||||
download_path?: string | null;
|
||||
}
|
||||
|
||||
export interface DocumentLike {
|
||||
id?: string | number;
|
||||
current_version?: DocumentVersion | null;
|
||||
}
|
||||
export type DocumentLike = OcrDocumentLike;
|
||||
|
||||
const asyncFalse = async () => false;
|
||||
|
||||
@@ -20,7 +16,7 @@ const resolveDocumentDownloadHref = (document: DocumentLike | null | undefined,
|
||||
if (!document || !resolveApiPath) {
|
||||
return null;
|
||||
}
|
||||
const downloadPath = document.current_version?.download_path;
|
||||
const downloadPath = (document.current_version as { download_path?: string | null } | null | undefined)?.download_path;
|
||||
if (!downloadPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export interface UseDocumentsPanelPropsArgs {
|
||||
clearDocumentSelection?: () => void;
|
||||
handleDeleteSelection?: () => void;
|
||||
handleEntryPointerCore?: (...args: unknown[]) => void;
|
||||
inspectDocument?: (...args: unknown[]) => void;
|
||||
inspectDocument?: (docId: Identifier | null, metadata?: unknown) => void;
|
||||
handleEntrySelection?: (...args: unknown[]) => void;
|
||||
tags?: unknown[];
|
||||
correspondents?: unknown[];
|
||||
|
||||
@@ -19,6 +19,8 @@ interface DocumentsPanelProps {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const defaultGetDocumentAsset = (_doc?: unknown, _type?: string) => null;
|
||||
|
||||
const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
||||
currentFolderName,
|
||||
breadcrumbs,
|
||||
@@ -49,7 +51,7 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
||||
activeCorrespondentIds = [],
|
||||
onFocusedRowChange,
|
||||
ensureAssetUrl = null,
|
||||
getDocumentAsset = () => null,
|
||||
getDocumentAsset = defaultGetDocumentAsset,
|
||||
onTagClick,
|
||||
onCorrespondentClick,
|
||||
isSearchLoading = false,
|
||||
@@ -207,7 +209,7 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
||||
);
|
||||
|
||||
const handleDocumentActivate = useCallback(
|
||||
(doc, event) => {
|
||||
(doc, event?: React.MouseEvent | KeyboardEvent | null) => {
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
@@ -223,7 +225,7 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
||||
handleDocumentPreviewZoom(doc);
|
||||
return;
|
||||
}
|
||||
onInspectDocument?.(doc.id, event);
|
||||
onInspectDocument?.(doc.id);
|
||||
},
|
||||
[handleDocumentPreviewZoom, onInspectDocument],
|
||||
);
|
||||
|
||||
@@ -128,8 +128,6 @@ const createDocumentsSurface = ({
|
||||
title,
|
||||
subtitle,
|
||||
sidebarToggle,
|
||||
parentBreadcrumb,
|
||||
onNavigateParent,
|
||||
actions,
|
||||
breadcrumbs,
|
||||
selectionLabel: null,
|
||||
|
||||
@@ -77,10 +77,22 @@ export const readTagTransferData = (dataTransfer: DataTransfer | null | undefine
|
||||
return null;
|
||||
};
|
||||
|
||||
type DragEventLike = DragEvent | { dataTransfer?: DataTransfer | null };
|
||||
type DragEventLike = DragEvent | DataTransfer | {
|
||||
dataTransfer?: DataTransfer | null;
|
||||
type?: string;
|
||||
preventDefault?: () => void;
|
||||
stopPropagation?: () => void;
|
||||
};
|
||||
|
||||
export const parseTagTransferPayload = (input: DataTransfer | DragEventLike | null): TagPayload | null => {
|
||||
const dataTransfer = input && 'dataTransfer' in input ? input.dataTransfer : input;
|
||||
let dataTransfer: DataTransfer | null = null;
|
||||
if (input) {
|
||||
if (typeof DataTransfer !== 'undefined' && input instanceof DataTransfer) {
|
||||
dataTransfer = input;
|
||||
} else if (typeof input === 'object' && 'dataTransfer' in input && input.dataTransfer) {
|
||||
dataTransfer = input.dataTransfer;
|
||||
}
|
||||
}
|
||||
const raw = readTagTransferData(dataTransfer || null);
|
||||
if (!raw) {
|
||||
return null;
|
||||
@@ -96,11 +108,19 @@ export const parseTagTransferPayload = (input: DataTransfer | DragEventLike | nu
|
||||
};
|
||||
|
||||
export const isTagTransferEvent = (event?: DragEventLike | null): boolean => {
|
||||
const types = event?.dataTransfer?.types;
|
||||
if (!event) {
|
||||
return false;
|
||||
}
|
||||
let types: DOMStringList | ReadonlyArray<string> | undefined;
|
||||
if (typeof DataTransfer !== 'undefined' && event instanceof DataTransfer) {
|
||||
types = event.types;
|
||||
} else if ('dataTransfer' in event && event.dataTransfer) {
|
||||
types = event.dataTransfer.types;
|
||||
}
|
||||
if (!types) {
|
||||
return false;
|
||||
}
|
||||
const typeList = Array.isArray(types) ? types : Array.from(types);
|
||||
const typeList = Array.isArray(types) ? [...types] : Array.from(types);
|
||||
return TAG_MIME_TYPES.some((type) => typeList.includes(type));
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user