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
@@ -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;