typescript

This commit is contained in:
2025-11-13 02:37:16 +01:00
parent ada089c05b
commit 6d55e61cee
43 changed files with 923 additions and 406 deletions
+2 -2
View File
@@ -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;
+6 -6
View File
@@ -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;
+6 -6
View File
@@ -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}
</>
);
};
+8 -12
View File
@@ -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,
+24 -4
View File
@@ -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));
};