refactor: Introduce dedicated Document and Asset types and migrate codebase from DocumentLike.

This commit is contained in:
2025-11-26 00:38:01 +01:00
parent 93dcde471f
commit 342714528a
31 changed files with 282 additions and 442 deletions
+8 -14
View File
@@ -9,13 +9,7 @@ import type { DocumentId } from '../types/identifiers';
type FolderId = DocumentId | 'root'; type FolderId = DocumentId | 'root';
type DocumentLike = { import type { Document } from '../types/documents';
id?: DocumentId;
folder_id?: FolderId | null;
filename?: string | null;
current_version?: Record<string, unknown>;
[key: string]: unknown;
};
type DocumentLink = { type DocumentLink = {
url?: string; url?: string;
@@ -29,11 +23,11 @@ type NavigateHandler = (path: string, options?: { replace?: boolean }) => void;
interface UseDocumentPreviewArgs { interface UseDocumentPreviewArgs {
routeDocumentId?: DocumentId | null; routeDocumentId?: DocumentId | null;
documentsManager: { documentsManager: {
getById: (id: DocumentId) => DocumentLike | null; getById: (id: DocumentId) => Document | null;
ensure: (id: DocumentId) => Promise<DocumentLike | null>; ensure: (id: DocumentId) => Promise<Document | null>;
getMany: (ids: DocumentId[]) => DocumentLike[]; getMany: (ids: DocumentId[]) => Document[];
subscribe: (listener: () => void) => () => void; subscribe: (listener: () => void) => () => void;
ingest: (docs: unknown[]) => { canonical: DocumentLike[]; changed: boolean }; ingest: (docs: unknown[]) => { canonical: Document[]; changed: boolean };
}; };
selectedFolder?: FolderId | null; selectedFolder?: FolderId | null;
notifyApiError: (error: unknown, message: string) => void; notifyApiError: (error: unknown, message: string) => void;
@@ -50,7 +44,7 @@ interface UseDocumentPreviewArgs {
interface UseDocumentPreviewResult { interface UseDocumentPreviewResult {
documentLinks: Map<DocumentId, DocumentLink>; documentLinks: Map<DocumentId, DocumentLink>;
ensureDownloadUrl: (documentId: DocumentId, options?: { force?: boolean }) => Promise<DocumentLink | null>; ensureDownloadUrl: (documentId: DocumentId, options?: { force?: boolean }) => Promise<DocumentLink | null>;
ensurePreviewData: (documentId: DocumentId) => Promise<DocumentLike | null>; ensurePreviewData: (documentId: DocumentId) => Promise<Document | null>;
openDocumentPreview: (documentId: DocumentId, options?: { replace?: boolean }) => void; openDocumentPreview: (documentId: DocumentId, options?: { replace?: boolean }) => void;
closeDocumentPreview: (folderId?: FolderId) => void; closeDocumentPreview: (folderId?: FolderId) => void;
resetPreviewState: () => void; resetPreviewState: () => void;
@@ -149,7 +143,7 @@ const useDocumentPreview = ({
); );
const ensurePreviewData = useCallback( const ensurePreviewData = useCallback(
async (documentId: DocumentId): Promise<DocumentLike | null> => { async (documentId: DocumentId): Promise<Document | null> => {
if (!documentId) return null; if (!documentId) return null;
const findInCache = () => documentsManager.getById(documentId); const findInCache = () => documentsManager.getById(documentId);
@@ -163,7 +157,7 @@ const useDocumentPreview = ({
if (!doc) { if (!doc) {
const fetched = await fetchDocument(documentId); const fetched = await fetchDocument(documentId);
const { canonical } = documentsManager.ingest([fetched as unknown]); const { canonical } = documentsManager.ingest([fetched as unknown]);
doc = (canonical[0] as DocumentLike | undefined) || null; doc = (canonical[0] as Document | undefined) || null;
if (!doc) { if (!doc) {
throw new Error('Document metadata unavailable.'); throw new Error('Document metadata unavailable.');
} }
+2 -2
View File
@@ -4,7 +4,7 @@ import { TAG_FILTER_UNTAGGED } from './workspaceUtils';
import { listDocuments } from '../lib/apiClient'; import { listDocuments } from '../lib/apiClient';
import type { Identifier } from '../types/identifiers'; import type { Identifier } from '../types/identifiers';
type DocumentLike = { id?: Identifier } & Record<string, unknown>; import type { Document } from '../types/documents';
type ApiClient = { type ApiClient = {
get: <T = unknown>(url: string, config?: { params?: Record<string, unknown> }) => Promise<{ data: T }>; get: <T = unknown>(url: string, config?: { params?: Record<string, unknown> }) => Promise<{ data: T }>;
@@ -23,7 +23,7 @@ interface UseDocumentsSearchArgs {
notifyApiError: (error: unknown, message: string) => void; notifyApiError: (error: unknown, message: string) => void;
setSearchIncludeDescendants: (value: boolean) => void; setSearchIncludeDescendants: (value: boolean) => void;
documentsManager: { documentsManager: {
ingest: (docs: unknown[]) => { canonical: DocumentLike[]; changed: boolean }; ingest: (docs: unknown[]) => { canonical: Document[]; changed: boolean };
}; };
} }
+6 -33
View File
@@ -1,37 +1,10 @@
import type { Identifier } from './types/identifiers'; import type { Identifier } from './types/identifiers';
import type { AssetObject, AssetLike } from './types/assets';
import type { DocumentVersion, Document } from './types/documents';
type Nullable<T> = T | null; type Nullable<T> = T | null;
export interface AssetObject { export type { AssetObject, AssetLike, DocumentVersion as DocumentVersionLike, Document };
ordinal?: number;
url?: string | null;
metadata?: Record<string, unknown> | null;
expires_at?: number;
[key: string]: unknown;
}
export interface AssetLike {
id?: Identifier;
asset_type?: string;
cardinality?: number | null;
download?: { url: string; expires_at: number } | null;
metadata?: Record<string, unknown> | null;
assets?: Record<string, AssetLike> | AssetLike[] | null;
objects?: AssetObject[] | null;
[key: string]: unknown;
}
export interface DocumentVersionLike {
assets?: Record<string, AssetLike> | AssetLike[] | null;
metadata?: Record<string, unknown> & { page_count?: number } | null;
[key: string]: unknown;
}
export interface DocumentLike {
id?: Identifier;
current_version?: DocumentVersionLike | null;
[key: string]: unknown;
}
export const resolveAssetExpiresAt = (asset?: { download?: { expires_at: number } | null } | null): number | null => export const resolveAssetExpiresAt = (asset?: { download?: { expires_at: number } | null } | null): number | null =>
asset?.download?.expires_at ?? null; asset?.download?.expires_at ?? null;
@@ -45,7 +18,7 @@ export type EnsureAssetUrl = (
options?: { force?: boolean;[key: string]: unknown }, options?: { force?: boolean;[key: string]: unknown },
) => Promise<unknown>; ) => Promise<unknown>;
export type GetAsset = (document: DocumentLike, assetType: string) => Nullable<AssetLike>; export type GetAsset = (document: Document, assetType: string) => Nullable<AssetLike>;
export const getAssetFromGroup = ( export const getAssetFromGroup = (
assets?: AssetLike[] | Record<string, AssetLike> | null, assets?: AssetLike[] | Record<string, AssetLike> | null,
@@ -62,7 +35,7 @@ export const getAssetFromGroup = (
return assets?.[assetType] || null; return assets?.[assetType] || null;
}; };
export const getAssetFromVersion = (currentVersion: Nullable<DocumentVersionLike>, assetType: string) => { export const getAssetFromVersion = (currentVersion: Nullable<DocumentVersion>, assetType: string) => {
if (!currentVersion) { if (!currentVersion) {
return null; return null;
} }
@@ -162,7 +135,7 @@ export class AssetView {
export const createAssetView = (asset?: AssetLike | null): AssetView => new AssetView(asset); export const createAssetView = (asset?: AssetLike | null): AssetView => new AssetView(asset);
export const resolveDocumentAssetUrl = ( export const resolveDocumentAssetUrl = (
doc: Nullable<DocumentLike>, doc: Nullable<Document>,
type: string, type: string,
{ {
ensureAssetUrl, ensureAssetUrl,
+5 -10
View File
@@ -5,12 +5,7 @@ import { getTagColorStyle } from '../utils/colors';
import { preventAll } from './events'; import { preventAll } from './events';
import type { DocumentId } from '../types/identifiers'; import type { DocumentId } from '../types/identifiers';
type DocumentLike = { import type { Document } from '../types/documents';
id?: string;
title?: string;
tags?: Array<{ id?: string; label?: string; color?: string | null }>;
[key: string]: unknown;
};
interface PendingRemovalTag { interface PendingRemovalTag {
docId?: string; docId?: string;
@@ -18,7 +13,7 @@ interface PendingRemovalTag {
} }
interface DesktopDocumentCardProps { interface DesktopDocumentCardProps {
doc: DocumentLike; doc: Document;
style?: React.CSSProperties; style?: React.CSSProperties;
shouldLoad?: boolean; shouldLoad?: boolean;
dragging?: boolean; dragging?: boolean;
@@ -35,9 +30,9 @@ interface DesktopDocumentCardProps {
onTagDragEnter?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void; onTagDragEnter?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
onTagDragOver?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void; onTagDragOver?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
onTagDragLeave?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void; onTagDragLeave?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
onTagDrop?: (event: React.DragEvent<HTMLDivElement>, doc: DocumentLike) => void; onTagDrop?: (event: React.DragEvent<HTMLDivElement>, doc: Document) => void;
onDocTagPointerDown?: (event: React.PointerEvent<HTMLElement>, doc: DocumentLike, tag: any) => void; onDocTagPointerDown?: (event: React.PointerEvent<HTMLElement>, doc: Document, tag: any) => void;
onDocTagDragStart?: (event: React.DragEvent<HTMLElement>, doc: DocumentLike, tag: any) => void; onDocTagDragStart?: (event: React.DragEvent<HTMLElement>, doc: Document, tag: any) => void;
onDocTagDrag?: (event: React.DragEvent<HTMLElement>) => void; onDocTagDrag?: (event: React.DragEvent<HTMLElement>) => void;
onDocTagDragEnd?: (event: React.DragEvent<HTMLElement>) => void; onDocTagDragEnd?: (event: React.DragEvent<HTMLElement>) => void;
pendingRemovalTag?: PendingRemovalTag | null; pendingRemovalTag?: PendingRemovalTag | null;
+3 -8
View File
@@ -2,12 +2,7 @@ import { useEffect } from 'react';
import type { JSX } from 'react'; import type { JSX } from 'react';
import { useAssetNavigator } from '../hooks/useAssetNavigator'; import { useAssetNavigator } from '../hooks/useAssetNavigator';
import type { Identifier } from '../types/identifiers'; import type { Identifier } from '../types/identifiers';
import type { Document } from '../types/documents';
interface DocumentLike {
id?: Identifier;
title?: string;
[key: string]: unknown;
}
interface AssetLike { interface AssetLike {
id?: Identifier; id?: Identifier;
@@ -23,7 +18,7 @@ type EnsureAssetUrl = (
options?: { force?: boolean;[key: string]: unknown }, options?: { force?: boolean;[key: string]: unknown },
) => Promise<unknown>; ) => Promise<unknown>;
type GetDocumentAsset = (document: DocumentLike | null, assetType: string) => AssetLike | null; type GetDocumentAsset = (document: Document | null, assetType: string) => AssetLike | null;
interface NavigatorSnapshot { interface NavigatorSnapshot {
url: string | null; url: string | null;
@@ -33,7 +28,7 @@ interface NavigatorSnapshot {
} }
interface DesktopPreviewCardProps { interface DesktopPreviewCardProps {
doc: DocumentLike | null; doc: Document | null;
title?: string; title?: string;
ensureAssetUrl?: EnsureAssetUrl | null; ensureAssetUrl?: EnsureAssetUrl | null;
getDocumentAsset: GetDocumentAsset; getDocumentAsset: GetDocumentAsset;
+6 -3
View File
@@ -173,6 +173,7 @@ interface DesktopWorkspaceViewProps extends Omit<DocumentsViewProps, 'entries' |
markLayoutDirty: () => void; markLayoutDirty: () => void;
onSelect?: (descriptor: unknown, event?: unknown) => void; onSelect?: (descriptor: unknown, event?: unknown) => void;
onPromoteSelection?: (docId: Identifier, event?: unknown) => void; onPromoteSelection?: (docId: Identifier, event?: unknown) => void;
selectionOrderRef: React.MutableRefObject<string[]>;
} }
const defaultGetDocumentAsset: GetAsset = () => null; const defaultGetDocumentAsset: GetAsset = () => null;
@@ -194,6 +195,7 @@ const DesktopWorkspace: React.FC<DocumentsViewProps> = ({
clearSelection, clearSelection,
handleEntrySelection, handleEntrySelection,
promoteSelectionOrder, promoteSelectionOrder,
selectionOrderRef,
configureSelectionEnvironment, configureSelectionEnvironment,
} = useWorkspaceSelectionContext(); } = useWorkspaceSelectionContext();
const items = useMemo<DeskDocument[]>( const items = useMemo<DeskDocument[]>(
@@ -814,6 +816,7 @@ const DesktopWorkspace: React.FC<DocumentsViewProps> = ({
recalcVisibleDocIds, recalcVisibleDocIds,
dragSettings, dragSettings,
markLayoutDirty, markLayoutDirty,
selectionOrderRef,
}; };
return <DesktopWorkspaceView {...viewProps} />; return <DesktopWorkspaceView {...viewProps} />;
}; };
@@ -850,13 +853,14 @@ function DesktopWorkspaceView({
resolveBaseMetrics, resolveBaseMetrics,
bringToFront, bringToFront,
setDraggingId, setDraggingId,
canvasSize, canvasSize: _canvasSize,
openOverlayForDoc, openOverlayForDoc,
recalcVisibleDocIds, recalcVisibleDocIds,
dragSettings, dragSettings,
onDocumentActivate, onDocumentActivate,
markLayoutDirty, markLayoutDirty,
onSelect, onSelect,
selectionOrderRef,
}: DesktopWorkspaceViewProps) { }: DesktopWorkspaceViewProps) {
const handleDeskDocumentActivate = useCallback( const handleDeskDocumentActivate = useCallback(
(docId: Identifier) => { (docId: Identifier) => {
@@ -883,19 +887,18 @@ function DesktopWorkspaceView({
engine, engine,
layoutRef, layoutRef,
dragTransformsRef, dragTransformsRef,
itemRefs,
documentLookup, documentLookup,
ensureDocumentSize, ensureDocumentSize,
resolveBaseMetrics, resolveBaseMetrics,
bringToFront, bringToFront,
setDraggingId, setDraggingId,
canvasSize,
openOverlayForDoc, openOverlayForDoc,
recalcVisibleDocIds, recalcVisibleDocIds,
settings: dragSettings, settings: dragSettings,
containerRef, containerRef,
onDocumentActivate: handleDeskDocumentActivate, onDocumentActivate: handleDeskDocumentActivate,
markLayoutDirty, markLayoutDirty,
selectionOrderRef,
selectedDocumentIds, selectedDocumentIds,
}) as { }) as {
handlePointerDown: (event: React.PointerEvent<HTMLElement>, docId: Identifier | null, options: PointerDownOptions) => void; handlePointerDown: (event: React.PointerEvent<HTMLElement>, docId: Identifier | null, options: PointerDownOptions) => void;
@@ -1,10 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import type { DocumentId } from '../../types/identifiers'; import type { DocumentId } from '../../types/identifiers';
interface DocumentLike { import type { Document } from '../../types/documents';
id?: string;
current_version?: unknown;
tags?: unknown;
}
interface AssetLike { interface AssetLike {
id?: string; id?: string;
@@ -17,11 +13,11 @@ interface PreviewMetadataEntry {
height: number; height: number;
} }
type GetDocumentAsset = (doc: DocumentLike, type: string) => AssetLike | null; type GetDocumentAsset = (doc: Document, type: string) => AssetLike | null;
type EnsureAssetUrl = (docId: DocumentId, asset: AssetLike, options?: { force?: boolean }) => Promise<AssetLike | null>; type EnsureAssetUrl = (docId: DocumentId, asset: AssetLike, options?: { force?: boolean }) => Promise<AssetLike | null>;
const usePreviewMetadata = ( const usePreviewMetadata = (
documents: DocumentLike[] | null, documents: Document[] | null,
getDocumentAsset?: GetDocumentAsset, getDocumentAsset?: GetDocumentAsset,
ensureAssetUrl?: EnsureAssetUrl, ensureAssetUrl?: EnsureAssetUrl,
) => { ) => {
@@ -37,7 +33,7 @@ const usePreviewMetadata = (
}; };
} }
const fetchMetadataForDoc = async (doc: DocumentLike) => { const fetchMetadataForDoc = async (doc: Document) => {
if (!doc?.id) { if (!doc?.id) {
return null; return null;
} }
+24 -60
View File
@@ -7,10 +7,8 @@ import {
} from 'react'; } from 'react';
import type { PointerEvent as ReactPointerEvent } from 'react'; import type { PointerEvent as ReactPointerEvent } from 'react';
import { preventAll } from './events'; import { preventAll } from './events';
import { clamp } from '../utils/math';
import usePointerTap from '../ui/usePointerTap'; import usePointerTap from '../ui/usePointerTap';
import { import {
applyDomTransform,
type WorkspaceEngine, type WorkspaceEngine,
type ActiveDragSession, type ActiveDragSession,
type DragGroupItem, type DragGroupItem,
@@ -18,18 +16,10 @@ import {
CARD_BASE_WEIGHT_GRAMS, CARD_BASE_WEIGHT_GRAMS,
CARD_PAGE_WEIGHT_GRAMS, CARD_PAGE_WEIGHT_GRAMS,
} from './workspaceEngine'; } from './workspaceEngine';
import { DRAG_HYSTERESIS_SQUARED, EDGE_COLLISION_THRESHOLD } from '../constants/desktop'; import { DRAG_HYSTERESIS_SQUARED } from '../constants/desktop';
import type { DocumentId, Identifier } from '../types/identifiers'; import type { Identifier } from '../types/identifiers';
import type { Document } from '../types/documents';
interface DocumentLike { import { getEntryId, isDocumentEntry } from '../app/entryKey';
id?: Identifier | null;
title?: string;
current_version?: {
metadata?: { page_count?: number | string | null } | null;
} | null;
metadata?: { page_count?: number | string | null } | null;
[key: string]: unknown;
}
interface DocumentSizeInfo { interface DocumentSizeInfo {
width: number; width: number;
@@ -56,10 +46,10 @@ interface DragTransform {
type EnsureDocumentSizeFn = (doc: DocumentLike | null) => DocumentSizeInfo | null; type EnsureDocumentSizeFn = (doc: Document | null) => DocumentSizeInfo | null;
type ResolveBaseMetricsFn = ( type ResolveBaseMetricsFn = (
doc: DocumentLike | null, doc: Document | null,
width: number, width: number,
height: number, height: number,
) => { baseWidth: number; baseHeight: number; baseScale: number }; ) => { baseWidth: number; baseHeight: number; baseScale: number };
@@ -81,13 +71,12 @@ interface UseDocumentDragOptions {
engine?: WorkspaceEngine | null; engine?: WorkspaceEngine | null;
layoutRef: MutableRefObject<Map<string, LayoutEntry>>; layoutRef: MutableRefObject<Map<string, LayoutEntry>>;
dragTransformsRef: MutableRefObject<Map<string, DragTransform>>; dragTransformsRef: MutableRefObject<Map<string, DragTransform>>;
itemRefs: MutableRefObject<Map<string, HTMLElement | null>>; selectionOrderRef?: MutableRefObject<string[]>;
documentLookup: Map<string, DocumentLike>; documentLookup: Map<string, Document>;
ensureDocumentSize: EnsureDocumentSizeFn; ensureDocumentSize: EnsureDocumentSizeFn;
resolveBaseMetrics: ResolveBaseMetricsFn; resolveBaseMetrics: ResolveBaseMetricsFn;
bringToFront: (docId: Identifier | null) => void; bringToFront: (docId: Identifier | null) => void;
setDraggingId: (docKey: string | null) => void; setDraggingId: (docKey: string | null) => void;
canvasSize: { width: number; height: number };
openOverlayForDoc?: ( openOverlayForDoc?: (
docId: Identifier | null, docId: Identifier | null,
originInfo?: { rotation: number; scale: number; width: number; height: number }, originInfo?: { rotation: number; scale: number; width: number; height: number },
@@ -107,8 +96,8 @@ interface DragTapMetadata {
docTitle: string; docTitle: string;
} }
const getDocumentPageCount = (doc?: DocumentLike | null): number | null => { const getDocumentPageCount = (doc?: Document | null): number | null => {
const raw = doc?.current_version?.metadata?.page_count ?? doc?.metadata?.page_count; const raw = doc?.current_version?.metadata?.page_count ?? (doc?.metadata as { page_count?: unknown })?.page_count;
if (raw == null) { if (raw == null) {
return null; return null;
} }
@@ -116,7 +105,7 @@ const getDocumentPageCount = (doc?: DocumentLike | null): number | null => {
return Number.isFinite(value) ? value : null; return Number.isFinite(value) ? value : null;
}; };
const computeDocumentMassGrams = (doc?: DocumentLike | null): number => { const computeDocumentMassGrams = (doc?: Document | null): number => {
const pages = Math.max(1, Math.round(getDocumentPageCount(doc) ?? 1)); const pages = Math.max(1, Math.round(getDocumentPageCount(doc) ?? 1));
return CARD_BASE_WEIGHT_GRAMS + pages * CARD_PAGE_WEIGHT_GRAMS; return CARD_BASE_WEIGHT_GRAMS + pages * CARD_PAGE_WEIGHT_GRAMS;
}; };
@@ -145,19 +134,18 @@ const useDocumentDrag = (options: UseDocumentDragOptions & { selectedDocumentIds
engine, engine,
layoutRef, layoutRef,
dragTransformsRef, dragTransformsRef,
itemRefs,
documentLookup, documentLookup,
ensureDocumentSize, ensureDocumentSize,
resolveBaseMetrics, resolveBaseMetrics,
bringToFront, bringToFront,
setDraggingId, setDraggingId,
canvasSize,
openOverlayForDoc, openOverlayForDoc,
recalcVisibleDocIds, recalcVisibleDocIds,
settings, settings,
containerRef: providedContainerRef, containerRef: providedContainerRef,
onDocumentActivate, onDocumentActivate,
markLayoutDirty, markLayoutDirty,
selectionOrderRef,
selectedDocumentIds, selectedDocumentIds,
} = options; } = options;
@@ -166,8 +154,6 @@ const useDocumentDrag = (options: UseDocumentDragOptions & { selectedDocumentIds
const { const {
canvasPadding = 24, canvasPadding = 24,
defaultCanvasWidth = 1024,
defaultCanvasHeight = 680,
debugDrag = false, debugDrag = false,
} = settings || {}; } = settings || {};
@@ -195,21 +181,6 @@ const useDocumentDrag = (options: UseDocumentDragOptions & { selectedDocumentIds
const dragStateRef = useRef<ActiveDragSession | null>(null); const dragStateRef = useRef<ActiveDragSession | null>(null);
const pendingDragRef = useRef<PendingDrag | null>(null); const pendingDragRef = useRef<PendingDrag | null>(null);
const setDragTransform = useCallback((docKey: Identifier | null, transform: DragTransform | null) => {
if (!docKey) {
return;
}
const map = dragTransformsRef?.current;
if (!map) {
return;
}
if (transform) {
map.set(String(docKey), transform);
} else {
map.delete(String(docKey));
}
}, [dragTransformsRef]);
const clearDragTransforms = useCallback(() => { const clearDragTransforms = useCallback(() => {
const map = dragTransformsRef?.current; const map = dragTransformsRef?.current;
if (!map?.clear) { if (!map?.clear) {
@@ -268,27 +239,19 @@ const useDocumentDrag = (options: UseDocumentDragOptions & { selectedDocumentIds
); );
const startDragSession = useCallback((pending: PendingDrag, event: PointerEventLike) => { const startDragSession = useCallback((pending: PendingDrag, event: PointerEventLike) => {
const { docId: docIdInput, modifierActive, wasSelected } = pending; const { docId: docIdInput, modifierActive } = pending;
// 1. Get current global selection const selectionFromRef: string[] = Array.isArray(selectionOrderRef?.current)
let selectionIds: string[] = (selectedDocumentIds || []).map(String); ? selectionOrderRef.current
const docKey = String(docIdInput); .map((key) => (isDocumentEntry(key) ? getEntryId(key) : null))
.filter((id): id is string => Boolean(id))
.map(String)
: [];
// 2. Check if clicked doc was already selected BEFORE the click // 1. Get current global selection (prefer ref for immediate updates)
if (!wasSelected) { let selectionIds: string[] = selectionFromRef.length
// Not selected before click. Determine what SHOULD be dragged. ? selectionFromRef
const targets = (pending.stackHits && pending.stackHits.length > 0) : (selectedDocumentIds || []).map(String);
? pending.stackHits
: [docKey];
if (modifierActive) {
// Add to selection
selectionIds = [...selectionIds, ...targets];
} else {
// Replace selection
selectionIds = targets;
}
}
// 3. Filter for valid documents // 3. Filter for valid documents
selectionIds = selectionIds.filter((id, index, array) => array.indexOf(id) === index && documentLookup.has(id)); selectionIds = selectionIds.filter((id, index, array) => array.indexOf(id) === index && documentLookup.has(id));
@@ -472,6 +435,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions & { selectedDocumentIds
}, [ }, [
selectedDocumentIds, selectedDocumentIds,
selectionOrderRef,
documentLookup, documentLookup,
layoutRef, layoutRef,
ensureDocumentSize, ensureDocumentSize,
+2 -5
View File
@@ -1,12 +1,9 @@
import { clamp, formatTransform, toNumber } from '../utils/math'; import { clamp, formatTransform } from '../utils/math';
import { import {
Point,
Polygon, Polygon,
clipPolygon, clipPolygon,
isPointInsideConvex, isPointInsideConvex,
polygonCentroid, polygonCentroid,
iterateEdges,
forEachVertex,
} from './utils/geometry'; } from './utils/geometry';
import { computeCardBounds } from './utils/layoutUtils'; import { computeCardBounds } from './utils/layoutUtils';
import { fetchLayoutRecords, upsertLayoutRecords } from './db'; import { fetchLayoutRecords, upsertLayoutRecords } from './db';
@@ -631,7 +628,7 @@ export class WorkspaceEngine {
return this.state.type === 'dragging' ? this.state.session : null; return this.state.type === 'dragging' ? this.state.session : null;
} }
beginDrag(docIds: Array<string | null> = []): void { beginDrag(_docIds: Array<string | null> = []): void {
// Legacy method support or internal helper // Legacy method support or internal helper
// If we are starting a drag, we should transition state // If we are starting a drag, we should transition state
// But this method was used to set flags. // But this method was used to set flags.
+5 -10
View File
@@ -3,17 +3,12 @@ import { createPortal } from 'react-dom';
import { clamp } from '../utils/math'; import { clamp } from '../utils/math';
import PdfViewer from '../preview/PdfViewer'; import PdfViewer from '../preview/PdfViewer';
type DocumentLike = { import type { Document } from '../types/documents';
id?: string;
title?: string;
mime_type?: string | null;
[key: string]: unknown;
};
interface PreviewZoomOverlayProps { interface PreviewZoomOverlayProps {
open?: boolean; open?: boolean;
onClose?: () => void; onClose?: () => void;
document?: DocumentLike | null; document?: Document | null;
} }
type NaturalSize = { width: number | null; height: number | null }; type NaturalSize = { width: number | null; height: number | null };
@@ -26,7 +21,7 @@ type DocumentLink = {
mimeType?: string | null; mimeType?: string | null;
}; };
type DocumentLikeWithPreview = DocumentLike & { documentLink?: DocumentLink }; type DocumentWithPreview = Document & { documentLink?: DocumentLink };
const determineDisplayKind = (entry?: DocumentLink | null): DisplayKind => { const determineDisplayKind = (entry?: DocumentLink | null): DisplayKind => {
const type = entry?.mimeType?.toLowerCase?.() || ''; const type = entry?.mimeType?.toLowerCase?.() || '';
@@ -55,7 +50,7 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
const [naturalSize, setNaturalSize] = useState<NaturalSize>({ width: null, height: null }); const [naturalSize, setNaturalSize] = useState<NaturalSize>({ width: null, height: null });
const [renderBackdrop, setRenderBackdrop] = useState(false); const [renderBackdrop, setRenderBackdrop] = useState(false);
const [isBackdropVisible, setBackdropVisible] = useState(false); const [isBackdropVisible, setBackdropVisible] = useState(false);
const [documentSnapshot, setDocumentSnapshot] = useState<DocumentLikeWithPreview | null>(null); const [documentSnapshot, setDocumentSnapshot] = useState<DocumentWithPreview | null>(null);
const scrollRef = useRef<HTMLDivElement | null>(null); const scrollRef = useRef<HTMLDivElement | null>(null);
const mediaRef = useRef<HTMLImageElement | null>(null); const mediaRef = useRef<HTMLImageElement | null>(null);
const focusRef = useRef<FocusPoint>(null); const focusRef = useRef<FocusPoint>(null);
@@ -63,7 +58,7 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
const visibilityTimerRef = useRef<number | null>(null); const visibilityTimerRef = useRef<number | null>(null);
const displayTimerRef = useRef<number | null>(null); const displayTimerRef = useRef<number | null>(null);
const currentDocument = overlayDocument as DocumentLikeWithPreview | null; const currentDocument = overlayDocument;
useEffect(() => { useEffect(() => {
if (currentDocument?.documentLink?.url) { if (currentDocument?.documentLink?.url) {
+6 -12
View File
@@ -7,13 +7,7 @@ import { getEntryId, isDocumentEntry } from '../app/entryKey';
import type { DocumentInfoPanelProps } from '../documents/DocumentInfoPanel'; import type { DocumentInfoPanelProps } from '../documents/DocumentInfoPanel';
import type { EnsureAssetUrl, GetDocumentAsset } from '../utils/ocr'; import type { EnsureAssetUrl, GetDocumentAsset } from '../utils/ocr';
import type { Identifier } from '../types/identifiers'; import type { Identifier } from '../types/identifiers';
import type { Document } from '../types/documents';
interface DocumentLike {
id?: Identifier;
folder_id?: Identifier | 'root';
title?: string;
[key: string]: unknown;
}
interface FolderNode { interface FolderNode {
id: Identifier | 'root'; id: Identifier | 'root';
@@ -22,10 +16,10 @@ interface FolderNode {
} }
interface UseDetailWorkspaceArgs { interface UseDetailWorkspaceArgs {
documents: DocumentLike[]; documents: Document[];
selectionOrder: string[]; selectionOrder: string[];
selectedDocumentIds: Identifier[]; selectedDocumentIds: Identifier[];
documentLookup: Map<Identifier, DocumentLike>; documentLookup: Map<Identifier, Document>;
folderNodes: Map<Identifier | 'root', FolderNode>; folderNodes: Map<Identifier | 'root', FolderNode>;
ensureFolderData: (folderId: Identifier | 'root', options?: { force?: boolean; includeDocuments?: boolean }) => Promise<void>; ensureFolderData: (folderId: Identifier | 'root', options?: { force?: boolean; includeDocuments?: boolean }) => Promise<void>;
detailPanelControlRef: MutableRefObject<{ open?: (args?: { documentIds?: Identifier[] }) => void; close?: () => void } | null>; detailPanelControlRef: MutableRefObject<{ open?: (args?: { documentIds?: Identifier[] }) => void; close?: () => void } | null>;
@@ -35,7 +29,7 @@ interface UseDetailWorkspaceArgs {
openDocumentPreview?: (args: { documentIds: Identifier[] }) => void; openDocumentPreview?: (args: { documentIds: Identifier[] }) => void;
handleDocumentTitleUpdate?: (docId: Identifier, title: string) => Promise<boolean> | boolean; handleDocumentTitleUpdate?: (docId: Identifier, title: string) => Promise<boolean> | boolean;
handleDocumentIssuedUpdate?: (docId: Identifier, issued: number | null) => Promise<boolean> | boolean; handleDocumentIssuedUpdate?: (docId: Identifier, issued: number | null) => Promise<boolean> | boolean;
handleDocumentTagAdd?: (doc: DocumentLike, value: string, context?: { option?: unknown }) => void; handleDocumentTagAdd?: (doc: Document, value: string, context?: { option?: unknown }) => void;
handleTagRemove?: (...args: unknown[]) => void; handleTagRemove?: (...args: unknown[]) => void;
ensureAssetUrl?: EnsureAssetUrl; ensureAssetUrl?: EnsureAssetUrl;
getDocumentAsset?: GetDocumentAsset; getDocumentAsset?: GetDocumentAsset;
@@ -55,8 +49,8 @@ interface UseDetailWorkspaceResult {
handleDetailPanelClose: () => void; handleDetailPanelClose: () => void;
inspectDocument: (docId: Identifier | null) => void; inspectDocument: (docId: Identifier | null) => void;
previewActive: boolean; previewActive: boolean;
previewWorkspaceDocument: DocumentLike | null; previewWorkspaceDocument: Document | null;
resolveThumbnailUrlForDoc: (doc: DocumentLike | null) => string | null; resolveThumbnailUrlForDoc: (doc: Document | null) => string | null;
resolveFolderPath: (folderId?: Identifier | 'root') => Array<{ id: Identifier | 'root'; name: string }>; resolveFolderPath: (folderId?: Identifier | 'root') => Array<{ id: Identifier | 'root'; name: string }>;
} }
@@ -28,16 +28,7 @@ interface CorrespondentEntry {
count?: number; count?: number;
} }
interface DocumentLike { import type { Document } from '../types/documents';
id?: Identifier;
title?: string;
issued_at?: string | null;
folder_id?: FolderId | null;
current_version?: { version_number?: number } | null;
tags?: TagEntry[];
correspondents?: CorrespondentEntry[];
[key: string]: unknown;
}
interface TagSectionProps { interface TagSectionProps {
tags?: TagEntry[]; tags?: TagEntry[];
@@ -62,14 +53,14 @@ interface CorrespondentSectionProps {
} }
export interface DocumentSummarySectionProps { export interface DocumentSummarySectionProps {
document?: DocumentLike | null; document?: Document | null;
tagLookupById?: Map<TagId, TagEntry>; tagLookupById?: Map<TagId, TagEntry>;
tagOptions?: SelectionAssignmentMenuItem[]; tagOptions?: SelectionAssignmentMenuItem[];
onTagAdd?: (doc: DocumentLike, value: string, context?: { option?: unknown }) => void; onTagAdd?: (doc: Document, value: string, context?: { option?: unknown }) => void;
onTagRemove?: (docId: Identifier | undefined, tagId: TagId | undefined) => void; onTagRemove?: (docId: Identifier | undefined, tagId: TagId | undefined) => void;
correspondents?: CorrespondentEntry[]; correspondents?: CorrespondentEntry[];
correspondentOptions?: SelectionAssignmentMenuItem[]; correspondentOptions?: SelectionAssignmentMenuItem[];
onCorrespondentAdd?: (payload: { document: DocumentLike; name: string; option?: unknown }) => void; onCorrespondentAdd?: (payload: { document: Document; name: string; option?: unknown }) => void;
onCorrespondentRemove?: (payload: { documentId: Identifier | undefined; correspondentId: Identifier | undefined }) => void; onCorrespondentRemove?: (payload: { documentId: Identifier | undefined; correspondentId: Identifier | undefined }) => void;
onUpdateTitle?: (docId: Identifier | undefined, title: string) => Promise<boolean> | boolean; onUpdateTitle?: (docId: Identifier | undefined, title: string) => Promise<boolean> | boolean;
onUpdateIssued?: (docId: Identifier | undefined, timestamp: number | null) => Promise<boolean> | boolean; onUpdateIssued?: (docId: Identifier | undefined, timestamp: number | null) => Promise<boolean> | boolean;
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import type { CSSProperties, JSX, MutableRefObject } from 'react'; import type { CSSProperties, JSX, MutableRefObject } from 'react';
import type { Document } from '../types/documents';
import { import {
getAssetFromVersion, getAssetFromVersion,
resolveDocumentAssetUrl, resolveDocumentAssetUrl,
@@ -7,7 +8,6 @@ import {
} from '../asset_manager'; } from '../asset_manager';
import { DEFAULT_THUMBNAIL_SIZE } from '../constants/documents'; import { DEFAULT_THUMBNAIL_SIZE } from '../constants/documents';
import type { import type {
DocumentLike as AssetManagerDocumentLike,
AssetLike as AssetManagerAssetLike, AssetLike as AssetManagerAssetLike,
EnsureAssetUrl as AssetManagerEnsureAssetUrl, EnsureAssetUrl as AssetManagerEnsureAssetUrl,
GetAsset as AssetManagerGetAsset, GetAsset as AssetManagerGetAsset,
@@ -65,18 +65,19 @@ const useLazyVisibility = (
return { ref: targetRef, isVisible }; return { ref: targetRef, isVisible };
}; };
const getPageCount = (doc?: DocumentLike | null) => {
const getPageCount = (doc?: Document | null) => {
const count = doc?.current_version?.metadata?.page_count; const count = doc?.current_version?.metadata?.page_count;
return Number.isFinite(count) ? Number(count) : null; return Number.isFinite(count) ? Number(count) : null;
}; };
type DocumentLike = AssetManagerDocumentLike;
type AssetLike = AssetManagerAssetLike; type AssetLike = AssetManagerAssetLike;
type EnsureAssetUrl = AssetManagerEnsureAssetUrl; type EnsureAssetUrl = AssetManagerEnsureAssetUrl;
type GetDocumentAsset = AssetManagerGetAsset; type GetDocumentAsset = AssetManagerGetAsset;
interface DocumentThumbnailImageProps { interface DocumentThumbnailImageProps {
document?: DocumentLike | null; document?: Document | null;
ensureAssetUrl?: EnsureAssetUrl; ensureAssetUrl?: EnsureAssetUrl;
getDocumentAsset?: GetDocumentAsset; getDocumentAsset?: GetDocumentAsset;
alt?: string; alt?: string;
+6 -6
View File
@@ -9,9 +9,9 @@ import useInlineRename from './useInlineRename';
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext'; import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
import type { import type {
FolderLike, Folder as FolderLike,
DocumentLike, Document,
} from './DocumentsList'; } from '../types/documents';
import type { DocumentsViewProps } from './panel/DocumentsPanel'; import type { DocumentsViewProps } from './panel/DocumentsPanel';
interface DocumentsGridProps extends DocumentsViewProps { interface DocumentsGridProps extends DocumentsViewProps {
@@ -63,9 +63,9 @@ const DocumentsGrid: React.FC<DocumentsGridProps> = ({
submitEditing: submitDocumentEditing, submitEditing: submitDocumentEditing,
savingId: savingDocumentId, savingId: savingDocumentId,
attachInputRef: attachDocumentInputRef, attachInputRef: attachDocumentInputRef,
} = useInlineRename<DocumentLike>(onDocumentRename, { } = useInlineRename<Document>(onDocumentRename, {
getCurrentValue: (doc: DocumentLike) => doc?.title ?? '', getCurrentValue: (doc: Document) => doc?.title ?? '',
getEntityId: (doc: DocumentLike) => doc?.id ?? null, getEntityId: (doc: Document) => doc?.id ?? null,
}); });
const { const {
+11 -55
View File
@@ -1,5 +1,4 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import type { MouseEvent } from 'react';
import { FolderIcon, CheckIcon, CloseIcon } from '../ui/icons'; import { FolderIcon, CheckIcon, CloseIcon } from '../ui/icons';
import { getTagColorStyle } from '../utils/colors'; import { getTagColorStyle } from '../utils/colors';
import { formatDate } from '../utils/date'; import { formatDate } from '../utils/date';
@@ -9,54 +8,12 @@ import { resolveCorrespondents } from './correspondents';
import { writeTagTransferData, parseTagTransferPayload } from './tagTransfer'; import { writeTagTransferData, parseTagTransferPayload } from './tagTransfer';
import useInlineRename from './useInlineRename'; import useInlineRename from './useInlineRename';
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext'; import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
import type { Identifier } from '../types/identifiers';
import type { DocumentsViewProps } from './panel/DocumentsPanel'; import type { DocumentsViewProps } from './panel/DocumentsPanel';
import type {
export interface FolderLike { Document,
id?: Identifier | 'root'; Folder,
name?: string; } from '../types/documents';
}
export interface DocumentTag {
id?: Identifier;
label?: string;
color?: string | null;
}
export interface DocumentCorrespondent {
id?: Identifier;
name?: string;
count?: number;
}
export interface DocumentLike {
id?: Identifier;
title?: string;
issued_at?: string | null;
created_at?: string | null;
uploaded_at?: string | null;
tags?: DocumentTag[] | null;
correspondents?: DocumentCorrespondent[] | null;
}
export type FolderEntry = {
type: 'folder';
id: Identifier | 'root';
key: string;
folder: FolderLike;
};
export type DocumentEntry = {
type: 'document';
id: Identifier;
key: string;
document: DocumentLike;
};
export type DocumentsListEntry = FolderEntry | DocumentEntry;
export type FolderEventHandler = (folder: FolderLike, event: MouseEvent<HTMLTableRowElement>) => void;
export type DocumentEventHandler = (document: DocumentLike, event: MouseEvent<HTMLTableRowElement>) => void;
const DocumentsList: React.FC<DocumentsViewProps> = ({ const DocumentsList: React.FC<DocumentsViewProps> = ({
entries, entries,
@@ -105,9 +62,9 @@ const DocumentsList: React.FC<DocumentsViewProps> = ({
submitEditing: submitDocumentEditing, submitEditing: submitDocumentEditing,
savingId: savingDocumentId, savingId: savingDocumentId,
attachInputRef: attachDocumentInputRef, attachInputRef: attachDocumentInputRef,
} = useInlineRename<DocumentLike>(onDocumentRename, { } = useInlineRename<Document>(onDocumentRename, {
getCurrentValue: (doc: DocumentLike) => doc?.title ?? '', getCurrentValue: (doc: Document) => doc?.title ?? '',
getEntityId: (doc: DocumentLike) => doc?.id ?? null, getEntityId: (doc: Document) => doc?.id ?? null,
}); });
const { const {
@@ -119,9 +76,9 @@ const DocumentsList: React.FC<DocumentsViewProps> = ({
submitEditing: submitFolderEditing, submitEditing: submitFolderEditing,
savingId: savingFolderId, savingId: savingFolderId,
attachInputRef: attachFolderInputRef, attachInputRef: attachFolderInputRef,
} = useInlineRename<FolderLike>(onFolderRename, { } = useInlineRename<Folder>(onFolderRename, {
getCurrentValue: (folder: FolderLike) => folder?.name ?? '', getCurrentValue: (folder: Folder) => folder?.name ?? '',
getEntityId: (folder: FolderLike) => folder?.id ?? null, getEntityId: (folder: Folder) => folder?.id ?? null,
}); });
@@ -310,7 +267,6 @@ const DocumentsList: React.FC<DocumentsViewProps> = ({
data-doc-id={doc.id} data-doc-id={doc.id}
onClick={(event) => onDocumentClick?.(doc, event)} onClick={(event) => onDocumentClick?.(doc, event)}
onDoubleClick={(event) => onDocumentActivate?.(doc, event)} onDoubleClick={(event) => onDocumentActivate?.(doc, event)}
draggable
onDragStart={(event) => onDocumentDragStart?.(event, doc)} onDragStart={(event) => onDocumentDragStart?.(event, doc)}
onDragEnd={(event) => onDocumentDragEnd?.(event)} onDragEnd={(event) => onDocumentDragEnd?.(event)}
onDragOver={onDocumentTagDragOver} onDragOver={onDocumentTagDragOver}
@@ -40,12 +40,7 @@ interface CorrespondentOption {
label?: string; label?: string;
} }
interface DocumentLike { import type { Document } from '../types/documents';
id?: DocumentId;
tags?: TagOption[];
correspondents?: CorrespondentOption[];
[key: string]: unknown;
}
interface BulkTagMutationArgs { interface BulkTagMutationArgs {
label: string; label: string;
@@ -68,7 +63,7 @@ export interface SelectionFloatingActionsProps {
selectionCount?: number; selectionCount?: number;
selectedDocumentIds?: SelectedIdList; selectedDocumentIds?: SelectedIdList;
selectedFolderIds?: SelectedIdList; selectedFolderIds?: SelectedIdList;
documentLookup?: Map<DocumentId, DocumentLike> | null; documentLookup?: Map<DocumentId, Document> | null;
tags?: TagOption[] | null; tags?: TagOption[] | null;
tagLookupById?: Map<DocumentId, TagOption> | null; tagLookupById?: Map<DocumentId, TagOption> | null;
correspondents?: CorrespondentOption[] | null; correspondents?: CorrespondentOption[] | null;
@@ -135,7 +130,7 @@ const buildFolderTreeOptions = (tree?: FolderTreeNode[] | null): SelectionAssign
}; };
const buildTagAssignments = ( const buildTagAssignments = (
selectedDocuments: DocumentLike[], selectedDocuments: Document[],
tagLookupById: Map<DocumentId, TagOption> | null, tagLookupById: Map<DocumentId, TagOption> | null,
tags: TagOption[] | null, tags: TagOption[] | null,
total: number, total: number,
@@ -200,7 +195,7 @@ const buildTagAssignments = (
}; };
const buildCorrespondentAssignments = ( const buildCorrespondentAssignments = (
selectedDocuments: DocumentLike[], selectedDocuments: Document[],
correspondents: CorrespondentOption[] | null, correspondents: CorrespondentOption[] | null,
total: number, total: number,
): SelectionAssignmentMenuItem[] => { ): SelectionAssignmentMenuItem[] => {
@@ -280,7 +275,7 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
const tenantId = tenant?.id ?? null; const tenantId = tenant?.id ?? null;
const documentLookupMap = useMemo(() => ( const documentLookupMap = useMemo(() => (
documentLookup instanceof Map ? documentLookup : new Map<DocumentId, DocumentLike>() documentLookup instanceof Map ? documentLookup : new Map<DocumentId, Document>()
), [documentLookup]); ), [documentLookup]);
const tagLookupMap = tagLookupById instanceof Map ? tagLookupById : null; const tagLookupMap = tagLookupById instanceof Map ? tagLookupById : null;
@@ -350,13 +345,13 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
const folderCount = folderIdList.length; const folderCount = folderIdList.length;
const totalCount = selectionCount ?? documentCount + folderCount; const totalCount = selectionCount ?? documentCount + folderCount;
const selectedDocuments = useMemo<DocumentLike[]>(() => { const selectedDocuments = useMemo<Document[]>(() => {
if (!documentIdList.length || !(documentLookupMap instanceof Map)) { if (!documentIdList.length || !(documentLookupMap instanceof Map)) {
return []; return [];
} }
return documentIdList return documentIdList
.map((id) => documentLookupMap.get(id)) .map((id) => documentLookupMap.get(id))
.filter((doc): doc is DocumentLike => Boolean(doc)); .filter((doc): doc is Document => Boolean(doc));
}, [documentIdList, documentLookupMap]); }, [documentIdList, documentLookupMap]);
const selectedDocCount = selectedDocuments.length; const selectedDocCount = selectedDocuments.length;
+2 -4
View File
@@ -4,9 +4,7 @@ export interface CorrespondentReference {
key?: string; key?: string;
} }
export interface DocumentLike { import type { Document } from '../types/documents';
correspondents?: CorrespondentReference[];
}
export interface ResolvedCorrespondent { export interface ResolvedCorrespondent {
id?: string | null; id?: string | null;
@@ -14,7 +12,7 @@ export interface ResolvedCorrespondent {
key: string; key: string;
} }
export const resolveCorrespondents = (doc?: DocumentLike | null): ResolvedCorrespondent[] => { export const resolveCorrespondents = (doc?: Document | null): ResolvedCorrespondent[] => {
if (!doc || !Array.isArray(doc.correspondents)) { if (!doc || !Array.isArray(doc.correspondents)) {
return []; return [];
} }
+5 -5
View File
@@ -3,14 +3,14 @@ import type {
EnsureAssetUrl, EnsureAssetUrl,
EnsurePreviewData, EnsurePreviewData,
GetDocumentAsset, GetDocumentAsset,
DocumentLike as OcrDocumentLike,
} from '../utils/ocr'; } from '../utils/ocr';
import type { Document } from '../types/documents';
export type DocumentLike = OcrDocumentLike; export type { Document };
const asyncFalse = async () => false; const asyncFalse = async () => false;
const resolveDocumentDownloadHref = (document?: DocumentLike | null): string | null => { const resolveDocumentDownloadHref = (document?: Document | null): string | null => {
if (!document) { if (!document) {
return null; return null;
} }
@@ -21,7 +21,7 @@ const resolveDocumentDownloadHref = (document?: DocumentLike | null): string | n
return downloadUrl; return downloadUrl;
}; };
const hasDocumentOcrAsset = (document?: DocumentLike | null, getDocumentAsset?: GetDocumentAsset | null): boolean => { const hasDocumentOcrAsset = (document?: Document | null, getDocumentAsset?: GetDocumentAsset | null): boolean => {
if (!document || !getDocumentAsset) { if (!document || !getDocumentAsset) {
return false; return false;
} }
@@ -29,7 +29,7 @@ const hasDocumentOcrAsset = (document?: DocumentLike | null, getDocumentAsset?:
}; };
interface CreateDocumentActionStateArgs { interface CreateDocumentActionStateArgs {
document: DocumentLike | null; document: Document | null;
ensurePreviewData: EnsurePreviewData; ensurePreviewData: EnsurePreviewData;
ensureAssetUrl: EnsureAssetUrl; ensureAssetUrl: EnsureAssetUrl;
getDocumentAsset?: GetDocumentAsset | null; getDocumentAsset?: GetDocumentAsset | null;
+17 -53
View File
@@ -2,39 +2,7 @@ import { formatFileSize } from '../utils/format';
import { formatDateTime as defaultFormatDateTime } from '../utils/date'; import { formatDateTime as defaultFormatDateTime } from '../utils/date';
import { DEFAULT_FOLDER_NAME } from '../app/workspaceUtils'; import { DEFAULT_FOLDER_NAME } from '../app/workspaceUtils';
interface DocumentPageMetadata { import type { DocumentTag, DocumentCorrespondent, Document } from '../types/documents';
page_count?: number | string | null;
}
interface DocumentVersion {
size_bytes?: number | string | null;
metadata?: DocumentPageMetadata | null;
checksum?: string | null;
}
interface TagEntry {
label?: string | null;
}
interface CorrespondentEntry {
name?: string | null;
}
export interface SummaryDocument {
title?: string | null;
original_name?: string | null;
filename?: string | null;
mime_type?: string | null;
folder_id?: string | null;
folder_name?: string;
current_version?: DocumentVersion | null;
created_at?: string | null;
updated_at?: string | null;
issued_at?: string | null;
folder_path?: string;
tags?: TagEntry[] | null;
correspondents?: CorrespondentEntry[] | null;
}
interface DescribeSummaryOptions { interface DescribeSummaryOptions {
formatDateTime?: typeof defaultFormatDateTime; formatDateTime?: typeof defaultFormatDateTime;
@@ -51,7 +19,7 @@ export interface DocumentSummaryRow {
export type DocumentSummary = DocumentSummaryRow[]; export type DocumentSummary = DocumentSummaryRow[];
const coercePageCount = (metadata?: DocumentPageMetadata | null): number | null => { const coercePageCount = (metadata?: { page_count?: number | string | null } | null): number | null => {
const raw = metadata?.page_count; const raw = metadata?.page_count;
if (raw == null || raw === '') { if (raw == null || raw === '') {
return null; return null;
@@ -67,31 +35,26 @@ interface DocumentMetadataPayload {
[key: string]: unknown; [key: string]: unknown;
} }
export interface MetadataDocumentLike { export const describeDocumentSummary = (document?: Document | null, options: DescribeSummaryOptions = {}): DocumentSummary => {
created_at?: string | null;
updated_at?: string | null;
filename?: string | null;
original_name?: string | null;
mime_type?: string | null;
metadata?: DocumentMetadataPayload | null;
current_version?: { checksum?: string | null } | null;
}
export const describeDocumentSummary = (document?: SummaryDocument | null, options: DescribeSummaryOptions = {}): DocumentSummary => {
const { const {
formatDateTime = defaultFormatDateTime, formatDateTime = defaultFormatDateTime,
} = options; } = options;
const formatDateLabel = (value?: string | null) => formatDateTime(value) || '—'; const formatDateLabel = (value?: string | number | null) => {
const doc = document ?? {}; if (typeof value === 'number') {
return formatDateTime(new Date(value)) || '—';
}
return formatDateTime(value) || '—';
};
const doc = document ?? ({} as Document);
const sizeBytes = Number(doc.current_version?.size_bytes); const sizeBytes = Number(doc.current_version?.size_bytes);
const sizeLabel = Number.isFinite(sizeBytes) && sizeBytes > 0 ? formatFileSize(sizeBytes) : '—'; const sizeLabel = Number.isFinite(sizeBytes) && sizeBytes > 0 ? formatFileSize(sizeBytes) : '—';
const metadata = doc.current_version?.metadata || null; const metadata = doc.current_version?.metadata || null;
const pageCount = coercePageCount(metadata); const pageCount = coercePageCount(metadata);
const pageCountLabel = Number.isFinite(pageCount) ? String(pageCount) : '—'; const pageCountLabel = Number.isFinite(pageCount) ? String(pageCount) : '—';
const folderLabel = doc.folder_id == null ? DEFAULT_FOLDER_NAME : `Folder ${doc.folder_id}`; const folderLabel = doc.folder_id == null ? DEFAULT_FOLDER_NAME : `Folder ${doc.folder_id}`;
const tags = sanitizeArray<TagEntry>(doc.tags); const tags = sanitizeArray<DocumentTag>(doc.tags);
const correspondents = sanitizeArray<CorrespondentEntry>(doc.correspondents); const correspondents = sanitizeArray<DocumentCorrespondent>(doc.correspondents);
const tagLabels = tags.map((tag) => tag.label).filter(Boolean) as string[]; const tagLabels = tags.map((tag) => tag.label).filter(Boolean) as string[];
const correspondentLabels = correspondents.map((entry) => entry.name).filter(Boolean) as string[]; const correspondentLabels = correspondents.map((entry) => entry.name).filter(Boolean) as string[];
const tagsSummary = tagLabels.length ? tagLabels.join(', ') : '—'; const tagsSummary = tagLabels.length ? tagLabels.join(', ') : '—';
@@ -113,13 +76,14 @@ export const describeDocumentSummary = (document?: SummaryDocument | null, optio
]; ];
}; };
export const extractDocumentMetadataPayload = (document?: MetadataDocumentLike | null): DocumentMetadataPayload | null => { export const extractDocumentMetadataPayload = (document?: Document | null): DocumentMetadataPayload | null => {
if (!document?.metadata) { const metadata = document?.['metadata'] as DocumentMetadataPayload | undefined;
if (!metadata) {
return null; return null;
} }
const keys = Object.keys(document.metadata); const keys = Object.keys(metadata);
if (!keys.length) { if (!keys.length) {
return null; return null;
} }
return document.metadata; return metadata;
}; };
@@ -6,9 +6,9 @@ import type {
DocumentsListEntry, DocumentsListEntry,
FolderEventHandler, FolderEventHandler,
DocumentEventHandler, DocumentEventHandler,
DocumentLike, Document,
DocumentTag, DocumentTag,
} from '../DocumentsList'; } from '../../types/documents';
import DesktopWorkspace from '../../desktop/DesktopWorkspace'; import DesktopWorkspace from '../../desktop/DesktopWorkspace';
import { isTagTransferEvent } from '../tagTransfer'; import { isTagTransferEvent } from '../tagTransfer';
import PreviewZoomOverlay from '../../detail/PreviewZoomOverlay'; import PreviewZoomOverlay from '../../detail/PreviewZoomOverlay';
@@ -64,7 +64,7 @@ export interface DocumentsViewProps {
onFolderRename?: (folderId: Identifier | 'root', nextName: string) => Promise<boolean> | boolean; onFolderRename?: (folderId: Identifier | 'root', nextName: string) => Promise<boolean> | boolean;
onDocumentClick?: DocumentEventHandler; onDocumentClick?: DocumentEventHandler;
onDocumentActivate?: DocumentEventHandler; onDocumentActivate?: DocumentEventHandler;
onDocumentDragStart?: (event: DragEvent<HTMLElement>, document: DocumentLike) => void; onDocumentDragStart?: (event: DragEvent<HTMLElement>, document: Document) => void;
onDocumentDragEnd?: (event: DragEvent<HTMLElement>) => void; onDocumentDragEnd?: (event: DragEvent<HTMLElement>) => void;
onDocumentTagDragOver?: (event: DragEvent<HTMLElement>) => void; onDocumentTagDragOver?: (event: DragEvent<HTMLElement>) => void;
onDocumentTagDragLeave?: (event: DragEvent<HTMLElement>) => void; onDocumentTagDragLeave?: (event: DragEvent<HTMLElement>) => void;
@@ -6,11 +6,7 @@ import type { FolderId, Identifier } from '../../types/identifiers';
type FolderIdentifier = FolderId | 'root'; type FolderIdentifier = FolderId | 'root';
type FolderInput = FolderIdentifier | number; type FolderInput = FolderIdentifier | number;
interface DocumentLike { import type { Document } from '../../types/documents';
id?: Identifier | null;
title?: string;
[key: string]: unknown;
}
type ApplySelectionFn = ( type ApplySelectionFn = (
keys: string[], keys: string[],
@@ -28,7 +24,7 @@ interface UseDocumentDragHandlersOptions {
selectedFolderIds: FolderInput[]; selectedFolderIds: FolderInput[];
applySelection: ApplySelectionFn; applySelection: ApplySelectionFn;
handleEntrySelection: HandleEntrySelectionFn; handleEntrySelection: HandleEntrySelectionFn;
documentLookup: Map<Identifier, DocumentLike>; documentLookup: Map<Identifier, Document>;
setDraggedDocumentIds: (ids: Identifier[] | []) => void; setDraggedDocumentIds: (ids: Identifier[] | []) => void;
setDraggedFolderId: (id: FolderIdentifier | null) => void; setDraggedFolderId: (id: FolderIdentifier | null) => void;
documentsViewMode: string; documentsViewMode: string;
@@ -62,7 +58,7 @@ const useDocumentDragHandlers = ({
useEffect(() => destroyDragPreview, [destroyDragPreview]); useEffect(() => destroyDragPreview, [destroyDragPreview]);
const createDragPreview = useCallback( const createDragPreview = useCallback(
({ documents = [], folders = [] }: { documents?: DocumentLike[]; folders?: FolderIdentifier[] } = {}) => { ({ documents = [], folders = [] }: { documents?: Document[]; folders?: FolderIdentifier[] } = {}) => {
destroyDragPreview(); destroyDragPreview();
const docEntries = (documents || []).filter(Boolean); const docEntries = (documents || []).filter(Boolean);
@@ -202,9 +198,9 @@ const useDocumentDragHandlers = ({
); );
const handleDocumentDragStart = useCallback( const handleDocumentDragStart = useCallback(
(event: DragEvent<HTMLElement>, documentOrId: DocumentLike | Identifier | null) => { (event: DragEvent<HTMLElement>, documentOrId: Document | Identifier | null) => {
const documentId: Identifier | null = Object(documentOrId) === documentOrId const documentId: Identifier | null = Object(documentOrId) === documentOrId
? (documentOrId as DocumentLike)?.id ?? null ? (documentOrId as Document)?.id ?? null
: (documentOrId as Identifier | null); : (documentOrId as Identifier | null);
if (!documentId) { if (!documentId) {
return; return;
@@ -14,6 +14,7 @@ import {
updateDocument, updateDocument,
} from '../../lib/apiClient'; } from '../../lib/apiClient';
import type { DocumentId, FolderId as FolderIdentifier } from '../../types/identifiers'; import type { DocumentId, FolderId as FolderIdentifier } from '../../types/identifiers';
import type { Document } from '../../types/documents';
type FolderId = FolderIdentifier | 'root'; type FolderId = FolderIdentifier | 'root';
type NullableFolderId = FolderId | null; type NullableFolderId = FolderId | null;
@@ -21,8 +22,8 @@ type NullableFolderId = FolderId | null;
type StatusLevel = 'success' | 'error' | 'info' | string; type StatusLevel = 'success' | 'error' | 'info' | string;
type DocumentCacheMapper = ( type DocumentCacheMapper = (
doc: DocumentLike | null, doc: Document | null,
) => DocumentLike | null; ) => Document | null;
type MapDocumentCaches = (mapper: DocumentCacheMapper) => void; type MapDocumentCaches = (mapper: DocumentCacheMapper) => void;
@@ -53,19 +54,8 @@ interface Tag {
[key: string]: unknown; [key: string]: unknown;
} }
interface DocumentLike {
id?: DocumentId;
folder_id?: NullableFolderId;
folder_path?: string | null;
folder_name?: string | null;
issued_at?: number | null;
title?: string;
tags?: Tag[];
[key: string]: unknown;
}
interface FolderContents { interface FolderContents {
documents?: DocumentLike[]; documents?: Document[];
subfolders?: Array<{ id?: FolderId;[key: string]: unknown }>; subfolders?: Array<{ id?: FolderId;[key: string]: unknown }>;
[key: string]: unknown; [key: string]: unknown;
} }
@@ -109,12 +99,12 @@ interface FolderDeleteOptions {
interface UseDocumentMutationsArgs { interface UseDocumentMutationsArgs {
token?: string | null; token?: string | null;
documentLookup: Map<DocumentId, DocumentLike>; documentLookup: Map<DocumentId, Document>;
folderLabelMap: Map<FolderId, string>; folderLabelMap: Map<FolderId, string>;
ensureFolderData: EnsureFolderData; ensureFolderData: EnsureFolderData;
selectedFolder: FolderId; selectedFolder: FolderId;
setSelectedFolder: Dispatch<SetStateAction<FolderId>>; setSelectedFolder: Dispatch<SetStateAction<FolderId>>;
setDocuments: Dispatch<SetStateAction<DocumentLike[]>>; setDocuments: Dispatch<SetStateAction<Document[]>>;
setFolderContents: Dispatch<SetStateAction<Map<FolderId, FolderContents>>>; setFolderContents: Dispatch<SetStateAction<Map<FolderId, FolderContents>>>;
setSearchResultIds: Dispatch<SetStateAction<DocumentId[] | null>>; setSearchResultIds: Dispatch<SetStateAction<DocumentId[] | null>>;
setSelectedEntries: Dispatch<SetStateAction<string[]>>; setSelectedEntries: Dispatch<SetStateAction<string[]>>;
@@ -140,13 +130,13 @@ interface UseDocumentMutationsArgs {
tags: Tag[]; tags: Tag[];
refreshTags: () => Promise<void>; refreshTags: () => Promise<void>;
tagManager: TagManager; tagManager: TagManager;
extractDocumentFromResponse?: (payload: unknown) => DocumentLike | null; extractDocumentFromResponse?: (payload: unknown) => Document | null;
ingestDocuments?: (docs: unknown[]) => { canonical: DocumentLike[]; changed: boolean }; ingestDocuments?: (docs: unknown[]) => { canonical: Document[]; changed: boolean };
} }
interface UseDocumentMutationsResult { interface UseDocumentMutationsResult {
moveDocumentsToFolder: ( moveDocumentsToFolder: (
documentIds: Array<DocumentId | DocumentLike>, documentIds: Array<DocumentId | Document>,
targetFolderId?: NullableFolderId, targetFolderId?: NullableFolderId,
) => Promise<void>; ) => Promise<void>;
handleThumbnailRegeneration: (documentId: DocumentId) => Promise<void>; handleThumbnailRegeneration: (documentId: DocumentId) => Promise<void>;
@@ -155,7 +145,7 @@ interface UseDocumentMutationsResult {
options?: DeleteOptions, options?: DeleteOptions,
) => Promise<boolean>; ) => Promise<boolean>;
handleDocumentTagAdd: ( handleDocumentTagAdd: (
document: DocumentLike, document: Document,
label: string, label: string,
extras?: DocumentTagExtras | null, extras?: DocumentTagExtras | null,
) => Promise<void>; ) => Promise<void>;
@@ -218,7 +208,7 @@ const useDocumentMutations = ({
ingestDocuments, ingestDocuments,
}: UseDocumentMutationsArgs): UseDocumentMutationsResult => { }: UseDocumentMutationsArgs): UseDocumentMutationsResult => {
const moveDocumentsToFolder = useCallback( const moveDocumentsToFolder = useCallback(
async (documentIds: Array<DocumentId | DocumentLike>, targetFolderId?: NullableFolderId) => { async (documentIds: Array<DocumentId | Document>, targetFolderId?: NullableFolderId) => {
const uniqueIds = Array.from( const uniqueIds = Array.from(
new Set((documentIds || []).map((value) => normalizeDocumentId(value)).filter(Boolean) as DocumentId[]), new Set((documentIds || []).map((value) => normalizeDocumentId(value)).filter(Boolean) as DocumentId[]),
); );
@@ -241,9 +231,9 @@ const useDocumentMutations = ({
document: doc, document: doc,
}; };
}) })
.filter(Boolean) as Array<{ id: DocumentId; sourceFolderId: NullableFolderId; document: DocumentLike }>; .filter(Boolean) as Array<{ id: DocumentId; sourceFolderId: NullableFolderId; document: Document }>;
const updatedDocsMap = new Map<DocumentId, DocumentLike>(); const updatedDocsMap = new Map<DocumentId, Document>();
const resolveTargetName = () => { const resolveTargetName = () => {
if (!targetLabel) { if (!targetLabel) {
return null; return null;
@@ -257,7 +247,7 @@ const useDocumentMutations = ({
if (!document) { if (!document) {
return; return;
} }
const updated: DocumentLike = { const updated: Document = {
...document, ...document,
folder_id: target, folder_id: target,
}; };
@@ -572,7 +562,7 @@ const useDocumentMutations = ({
); );
const handleDocumentTagAdd = useCallback( const handleDocumentTagAdd = useCallback(
async (document: DocumentLike, label: string, extras: DocumentTagExtras | null = null) => { async (document: Document, label: string, extras: DocumentTagExtras | null = null) => {
const normalizedLabel = tagManager.normalizeLabel(label); const normalizedLabel = tagManager.normalizeLabel(label);
const optionCandidate = extras?.option ?? null; const optionCandidate = extras?.option ?? null;
const input = extras?.input ?? null; const input = extras?.input ?? null;
+9 -13
View File
@@ -8,20 +8,16 @@ import {
} from 'react'; } from 'react';
import DocumentsManager from '../../documents/DocumentsManager'; import DocumentsManager from '../../documents/DocumentsManager';
import type { DocumentId } from '../../types/identifiers'; import type { DocumentId } from '../../types/identifiers';
import type { Document } from '../../types/documents';
interface DocumentLike {
id?: DocumentId;
[key: string]: unknown;
}
interface FolderContentsEntry { interface FolderContentsEntry {
documents?: DocumentLike[]; documents?: Document[];
[key: string]: unknown; [key: string]: unknown;
} }
interface UseDocumentsOptions { interface UseDocumentsOptions {
setFolderContents: Dispatch<SetStateAction<Map<string, FolderContentsEntry>>>; setFolderContents: Dispatch<SetStateAction<Map<string, FolderContentsEntry>>>;
fetchDocumentById?: (id: DocumentId) => Promise<DocumentLike | null>; fetchDocumentById?: (id: DocumentId) => Promise<Document | null>;
} }
const useDocuments = ({ const useDocuments = ({
@@ -29,16 +25,16 @@ const useDocuments = ({
fetchDocumentById, fetchDocumentById,
}: UseDocumentsOptions) => { }: UseDocumentsOptions) => {
const managerRef = useRef( const managerRef = useRef(
new DocumentsManager<DocumentLike>(fetchDocumentById), new DocumentsManager<Document>(fetchDocumentById),
); );
const [documents, setDocumentsState] = useState<DocumentLike[]>([]); const [documents, setDocumentsState] = useState<Document[]>([]);
useEffect(() => { useEffect(() => {
managerRef.current.setFetcher(fetchDocumentById); managerRef.current.setFetcher(fetchDocumentById);
}, [fetchDocumentById]); }, [fetchDocumentById]);
const setDocuments = useCallback( const setDocuments = useCallback(
(value: DocumentLike[] | ((prev: DocumentLike[]) => DocumentLike[])) => { (value: Document[] | ((prev: Document[]) => Document[])) => {
setDocumentsState((prev) => { setDocumentsState((prev) => {
const resolved = typeof value === 'function' ? value(prev) : value; const resolved = typeof value === 'function' ? value(prev) : value;
if (!Array.isArray(resolved)) { if (!Array.isArray(resolved)) {
@@ -52,7 +48,7 @@ const useDocuments = ({
); );
const mapDocumentCaches = useCallback( const mapDocumentCaches = useCallback(
(mapper: (doc: DocumentLike) => DocumentLike | undefined) => { (mapper: (doc: Document) => Document | undefined) => {
managerRef.current.map(mapper); managerRef.current.map(mapper);
const lookupSnapshot = managerRef.current.getSnapshot(); const lookupSnapshot = managerRef.current.getSnapshot();
@@ -64,7 +60,7 @@ const useDocuments = ({
const next = prev.map((doc) => { const next = prev.map((doc) => {
const id = doc?.id; const id = doc?.id;
if (id != null && lookupSnapshot.has(id as DocumentId)) { if (id != null && lookupSnapshot.has(id as DocumentId)) {
const canonical = lookupSnapshot.get(id as DocumentId) as DocumentLike; const canonical = lookupSnapshot.get(id as DocumentId) as Document;
if (canonical !== doc) { if (canonical !== doc) {
changed = true; changed = true;
} }
@@ -95,7 +91,7 @@ const useDocuments = ({
const updatedDocs = docs.map((doc) => { const updatedDocs = docs.map((doc) => {
const id = doc?.id; const id = doc?.id;
if (id != null && lookupSnapshot.has(id as DocumentId)) { if (id != null && lookupSnapshot.has(id as DocumentId)) {
const canonical = lookupSnapshot.get(id as DocumentId) as DocumentLike; const canonical = lookupSnapshot.get(id as DocumentId) as Document;
if (canonical !== doc) { if (canonical !== doc) {
docsChanged = true; docsChanged = true;
} }
@@ -62,15 +62,11 @@ const noop = () => { };
type FolderId = FolderIdentifier | 'root'; type FolderId = FolderIdentifier | 'root';
interface DocumentLike { import type { Document } from '../../types/documents';
id?: DocumentId | null;
title?: string | null;
[key: string]: unknown;
}
interface FolderContentsEntry { interface FolderContentsEntry {
folder?: { id?: FolderId; name?: string | null } | null; folder?: { id?: FolderId; name?: string | null } | null;
documents?: DocumentLike[]; documents?: Document[];
subfolders?: Array<{ id?: FolderId; name?: string | null;[key: string]: unknown }>; subfolders?: Array<{ id?: FolderId; name?: string | null;[key: string]: unknown }>;
__includesDocuments?: boolean; __includesDocuments?: boolean;
__sortField?: string | null; __sortField?: string | null;
@@ -397,7 +393,7 @@ const useDocumentsWorkspace = ({
() => () =>
visibleDocumentIds visibleDocumentIds
.map((id) => documentLookup.get(id) || null) .map((id) => documentLookup.get(id) || null)
.filter((doc): doc is DocumentLike => Boolean(doc)), .filter((doc): doc is Document => Boolean(doc)),
[visibleDocumentIds, documentLookup], [visibleDocumentIds, documentLookup],
); );
@@ -9,14 +9,10 @@ import {
createFolderEntryKey, createFolderEntryKey,
} from '../../app/entryKey'; } from '../../app/entryKey';
import type { FolderId as FolderIdentifier, Identifier } from '../../types/identifiers'; import type { FolderId as FolderIdentifier, Identifier } from '../../types/identifiers';
import type { Document } from '../../types/documents';
type FolderId = FolderIdentifier | 'root'; type FolderId = FolderIdentifier | 'root';
interface DocumentLike {
id?: Identifier | null;
[key: string]: unknown;
}
interface FolderSummary { interface FolderSummary {
id?: FolderId; id?: FolderId;
name?: string; name?: string;
@@ -31,7 +27,7 @@ interface FolderSummary {
interface FolderContentsEntry { interface FolderContentsEntry {
folder?: FolderSummary | null; folder?: FolderSummary | null;
documents?: DocumentLike[]; documents?: Document[];
subfolders?: FolderSummary[]; subfolders?: FolderSummary[];
__includesDocuments?: boolean; __includesDocuments?: boolean;
__sortField?: string | null; __sortField?: string | null;
@@ -67,7 +63,7 @@ interface UseFolderTreeOptions {
documentsSortFieldRef: MutableRefObject<string>; documentsSortFieldRef: MutableRefObject<string>;
documentsSortDirectionRef: MutableRefObject<string>; documentsSortDirectionRef: MutableRefObject<string>;
selectionHelpers: SelectionHelpers; selectionHelpers: SelectionHelpers;
setDocuments: Dispatch<SetStateAction<DocumentLike[]>>; setDocuments: Dispatch<SetStateAction<Document[]>>;
setFolderContents: Dispatch<SetStateAction<Map<FolderId, FolderContentsEntry>>>; setFolderContents: Dispatch<SetStateAction<Map<FolderId, FolderContentsEntry>>>;
folderContentsRef: MutableRefObject<Map<FolderId, FolderContentsEntry>>; folderContentsRef: MutableRefObject<Map<FolderId, FolderContentsEntry>>;
} }
+5 -8
View File
@@ -1,11 +1,8 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { resolveAssetUrl } from '../asset_manager'; import { resolveAssetUrl } from '../asset_manager';
import type { Identifier } from '../types/identifiers'; import type { Identifier } from '../types/identifiers';
import type { Document } from '../types/documents';
type DocumentLike = {
id?: Identifier;
[key: string]: unknown;
};
type AssetObject = { type AssetObject = {
url?: string | null; url?: string | null;
@@ -29,7 +26,7 @@ type EnsureAssetUrl = (
options?: { force?: boolean;[key: string]: unknown }, options?: { force?: boolean;[key: string]: unknown },
) => Promise<unknown>; ) => Promise<unknown>;
type GetAsset = (document: DocumentLike, assetType: string) => AssetLike | null; type GetAsset = (document: Document, assetType: string) => AssetLike | null;
type AssetViewLike = { type AssetViewLike = {
url: string | null; url: string | null;
@@ -37,14 +34,14 @@ type AssetViewLike = {
}; };
interface UseAssetNavigatorOptions { interface UseAssetNavigatorOptions {
document?: DocumentLike | null; document?: Document | null;
assetType: string; assetType: string;
ensureAssetUrl?: EnsureAssetUrl | null; ensureAssetUrl?: EnsureAssetUrl | null;
getAsset?: GetAsset; getAsset?: GetAsset;
} }
interface AssetNavigatorReturn { interface AssetNavigatorReturn {
document: DocumentLike | null; document: Document | null;
documentId: Identifier | null; documentId: Identifier | null;
asset: AssetLike | null; asset: AssetLike | null;
assetType: string; assetType: string;
@@ -5,14 +5,7 @@ import { DownloadIcon } from '../ui/icons';
import PdfViewer from './PdfViewer'; import PdfViewer from './PdfViewer';
import { AUDIO_EXTENSIONS, VIDEO_EXTENSIONS } from '../constants/preview'; import { AUDIO_EXTENSIONS, VIDEO_EXTENSIONS } from '../constants/preview';
interface DocumentLike { import type { Document } from '../types/documents';
id?: string;
title?: string;
mime_type?: string;
filename?: string;
original_name?: string;
[key: string]: unknown;
}
interface DocumentLink { interface DocumentLink {
url?: string; url?: string;
@@ -36,7 +29,7 @@ interface ContentTabConfig {
type LayoutMode = 'split' | 'stacked' | (string & {}); type LayoutMode = 'split' | 'stacked' | (string & {});
interface DocumentViewerLayoutProps { interface DocumentViewerLayoutProps {
document?: DocumentLike | null; document?: Document | null;
documentLink?: DocumentLink | null; documentLink?: DocumentLink | null;
summaryProps?: Record<string, unknown>; summaryProps?: Record<string, unknown>;
metadataPayload?: unknown; metadataPayload?: unknown;
+6 -33
View File
@@ -29,53 +29,26 @@ import DocumentViewerLayout from './DocumentViewerLayout';
import useViewerLayoutMode from './useViewerLayoutMode'; import useViewerLayoutMode from './useViewerLayoutMode';
import { usePanelResizeBindings } from '../app/PanelManagerContext'; import { usePanelResizeBindings } from '../app/PanelManagerContext';
import type { DocumentId, FolderId } from '../types/identifiers'; import type { DocumentId, FolderId } from '../types/identifiers';
import type { Document } from '../types/documents';
interface DocumentLike { import type { AssetLike } from '../types/assets';
id?: DocumentId;
title?: string;
mime_type?: string | null;
issued_at?: string | null;
folder_id?: FolderId | null;
correspondents?: Array<{ id?: string; name?: string }>;
current_version?: {
version_number?: number;
download?: { url?: string | null; expires_at?: number } | null;
mime_type?: string | null;
filename?: string | null;
} | null;
documentLink?: {
url: string;
alt?: string;
mimeType?: string | null;
} | null;
[key: string]: unknown;
}
interface AssetLike {
id?: string;
url?: string | null;
metadata?: Record<string, unknown> | null;
[key: string]: unknown;
}
type SidebarMode = 'overlay' | 'inline'; type SidebarMode = 'overlay' | 'inline';
interface DocumentViewerPanelProps extends DocumentSummarySectionProps { interface DocumentViewerPanelProps extends DocumentSummarySectionProps {
document: DocumentLike | null; document: Document | null;
ensureAssetUrl?: (docId: DocumentId, asset: AssetLike, options?: { force?: boolean }) => Promise<unknown>; ensureAssetUrl?: (docId: DocumentId, asset: AssetLike, options?: { force?: boolean }) => Promise<unknown>;
getDocumentAsset?: (doc: DocumentLike | null, type: string) => AssetLike | null; getDocumentAsset?: (doc: Document | null, type: string) => AssetLike | null;
ensurePreviewData?: (docId: DocumentId, options?: { signal?: AbortSignal }) => Promise<DocumentLike | null>; ensurePreviewData?: (docId: DocumentId, options?: { signal?: AbortSignal }) => Promise<Document | null>;
notifyApiError?: (error: unknown, fallbackMessage?: string) => void; notifyApiError?: (error: unknown, fallbackMessage?: string) => void;
sidebarToggle?: ReactNode; sidebarToggle?: ReactNode;
onClosePanel?: () => void; onClosePanel?: () => void;
resolveFolderPath?: (doc: DocumentLike | null) => Array<{ id?: string; name?: string }>; resolveFolderPath?: (doc: Document | null) => Array<{ id?: string; name?: string }>;
variant?: 'viewer' | 'sidebar'; variant?: 'viewer' | 'sidebar';
onCollapsePanel?: () => void; onCollapsePanel?: () => void;
onMaximizePanel?: (args: { documentIds: Array<string> }) => void; onMaximizePanel?: (args: { documentIds: Array<string> }) => void;
sidebarMode?: SidebarMode; sidebarMode?: SidebarMode;
} }
export const createDocumentViewerHeaderActions = ({ export const createDocumentViewerHeaderActions = ({
document, document,
actionState, actionState,
+20
View File
@@ -0,0 +1,20 @@
import type { Identifier } from './identifiers';
export interface AssetObject {
ordinal?: number;
url?: string | null;
metadata?: Record<string, unknown> | null;
expires_at?: number;
[key: string]: unknown;
}
export interface AssetLike {
id?: Identifier;
asset_type?: string;
cardinality?: number | null;
download?: { url: string; expires_at: number } | null;
metadata?: Record<string, unknown> | null;
assets?: Record<string, AssetLike> | AssetLike[] | null;
objects?: AssetObject[] | null;
[key: string]: unknown;
}
+74
View File
@@ -0,0 +1,74 @@
import type { Identifier } from './identifiers';
import type { AssetLike } from './assets';
export interface DocumentTag {
id?: Identifier;
label?: string | null;
color?: string | null;
}
export interface DocumentCorrespondent {
id?: Identifier;
name?: string | null;
count?: number;
}
export interface DocumentVersion {
assets?: Record<string, AssetLike> | AssetLike[] | null;
metadata?: Record<string, unknown> & { page_count?: number } | null;
size_bytes?: number | string | null;
checksum?: string | null;
[key: string]: unknown;
}
export interface Document {
id?: Identifier;
title?: string | null;
original_name?: string | null;
filename?: string | null;
mime_type?: string | null;
issued_at?: string | number | null;
created_at?: string | null;
uploaded_at?: string | null;
updated_at?: string | null;
folder_id?: Identifier | null;
folder_name?: string;
folder_path?: string;
tags?: DocumentTag[] | null;
correspondents?: DocumentCorrespondent[] | null;
current_version?: DocumentVersion | null;
// Allow for other properties as we unify loosely typed interfaces
[key: string]: unknown;
}
export interface Folder {
id?: Identifier | 'root';
name?: string;
}
export type FolderEntry = {
type: 'folder';
id: Identifier | 'root';
key: string;
folder: Folder;
};
export type DocumentEntry = {
type: 'document';
id: Identifier;
key: string;
document: Document;
};
export type DocumentsListEntry = FolderEntry | DocumentEntry;
// Event Handlers
import type { MouseEvent } from 'react';
export type FolderEventHandler = (folder: Folder, event: MouseEvent<HTMLElement>) => void;
export type DocumentEventHandler = (document: Document, event: MouseEvent<HTMLElement>) => void;
+9 -11
View File
@@ -1,18 +1,16 @@
import { resolveDocumentAssetUrl, resolveAssetUrl } from '../asset_manager'; import { resolveDocumentAssetUrl, resolveAssetUrl } from '../asset_manager';
import type { import type {
DocumentLike as AssetManagerDocumentLike,
DocumentVersionLike,
AssetLike as AssetManagerAssetLike,
GetAsset as AssetManagerGetAsset, GetAsset as AssetManagerGetAsset,
} from '../asset_manager'; } from '../asset_manager';
import type {
Document,
DocumentVersion,
} from '../types/documents';
import type { AssetLike } from '../types/assets';
export interface DocumentLike extends AssetManagerDocumentLike { export type { Document, DocumentVersion, AssetLike };
current_version?: DocumentVersionLike | null;
}
export type AssetLike = AssetManagerAssetLike; export type EnsurePreviewData = (id: string) => Promise<Document | null>;
export type EnsurePreviewData = (id: string) => Promise<DocumentLike | null>;
export type EnsureAssetUrl = ( export type EnsureAssetUrl = (
id: string, id: string,
asset: AssetLike, asset: AssetLike,
@@ -21,13 +19,13 @@ export type EnsureAssetUrl = (
export type GetDocumentAsset = AssetManagerGetAsset; export type GetDocumentAsset = AssetManagerGetAsset;
interface ResolveOcrTextUrlOptions { interface ResolveOcrTextUrlOptions {
document: DocumentLike | null; document: Document | null;
ensurePreviewData?: EnsurePreviewData; ensurePreviewData?: EnsurePreviewData;
getDocumentAsset?: GetDocumentAsset; getDocumentAsset?: GetDocumentAsset;
ensureAssetUrl?: EnsureAssetUrl; ensureAssetUrl?: EnsureAssetUrl;
} }
const pickAsset = (doc?: DocumentLike | null, getDocumentAsset?: GetDocumentAsset): AssetLike | null => { const pickAsset = (doc?: Document | null, getDocumentAsset?: GetDocumentAsset): AssetLike | null => {
if (!doc || !getDocumentAsset) { if (!doc || !getDocumentAsset) {
return null; return null;
} }