refactor: Introduce dedicated Document and Asset types and migrate codebase from DocumentLike.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user