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