diff --git a/frontend/src/asset_manager.ts b/frontend/src/asset_manager.ts index 94f030a..7692cc7 100644 --- a/frontend/src/asset_manager.ts +++ b/frontend/src/asset_manager.ts @@ -2,6 +2,8 @@ import type { AxiosInstance } from 'axios'; export type Identifier = string | number; +type Nullable = T | null; + export interface AssetObject { ordinal?: number; url?: string | null; @@ -40,12 +42,12 @@ export type EnsureAssetUrl = ( options?: { start?: number | null; limit?: number | null; force?: boolean; [key: string]: unknown }, ) => Promise; -export type GetAsset = (document: DocumentLike, assetType: string) => AssetLike | null | undefined; +export type GetAsset = (document: DocumentLike, assetType: string) => Nullable; export const getAssetFromGroup = ( - assets: AssetLike[] | Record | null | undefined, - assetType: string, -): AssetLike | null => { + assets?: AssetLike[] | Record | null, + assetType: string = '', +): Nullable => { if (!assetType || !assets) { return null; } @@ -57,11 +59,11 @@ export const getAssetFromGroup = ( return assets?.[assetType] || null; }; -export const getAssetFromVersion = (currentVersion, assetType) => { +export const getAssetFromVersion = (currentVersion: Nullable, assetType: string) => { if (!currentVersion) { return null; } - return getAssetFromGroup(currentVersion.assets, assetType); + return getAssetFromGroup(currentVersion.assets ?? null, assetType); }; const normalizeAssetObjects = (objects?: AssetObject[] | null): AssetObject[] => { @@ -182,7 +184,7 @@ export class AssetView { export const createAssetView = (asset?: AssetLike | null): AssetView => new AssetView(asset); export const resolveDocumentAssetUrl = ( - doc: DocumentLike | null | undefined, + doc: Nullable, type: string, { ensureAssetUrl, @@ -195,7 +197,7 @@ export const resolveDocumentAssetUrl = ( ensureOptions?: { start?: number; limit?: number; [key: string]: unknown }; objectOrdinal?: number; } = {}, -): string | null => { +): Nullable => { if (!doc || !type) { return null; } @@ -251,13 +253,13 @@ class AssetManager { this.api = api; } - rememberAsset(entry: AssetLike | null | undefined) { + rememberAsset(entry?: Nullable) { if (entry?.id) { this.assetCache.set(entry.id, entry); } } - hydrateAsset(asset: AssetLike | null | undefined): AssetLike | null | undefined { + hydrateAsset(asset?: Nullable): Nullable { if (!asset || !asset.id) { return asset; } @@ -287,7 +289,7 @@ class AssetManager { return merged; } - hydrateDocument(document: DocumentLike | null | undefined): DocumentLike | null | undefined { + hydrateDocument(document?: Nullable): Nullable { if (!document) { return document; } @@ -330,14 +332,14 @@ class AssetManager { return { ...document, current_version: nextCurrentVersion }; } - hydrateDocuments(documents: DocumentLike[] | null | undefined) { + hydrateDocuments(documents?: DocumentLike[] | null) { if (!Array.isArray(documents)) { - return documents; + return documents ?? []; } return documents.map((doc) => this.hydrateDocument(doc)); } - hydrateDetail(detail: { document?: DocumentLike; assets?: AssetLike[] } | null | undefined) { + hydrateDetail(detail?: { document?: DocumentLike; assets?: AssetLike[] } | null) { if (!detail) { return detail; } @@ -366,10 +368,7 @@ class AssetManager { return changed ? next : detail; } - hydrateFolderContents(contents: { - documents?: DocumentLike[]; - document?: DocumentLike; - } | null | undefined) { + hydrateFolderContents(contents?: { documents?: DocumentLike[]; document?: DocumentLike } | null) { if (!contents) { return contents; } @@ -384,12 +383,12 @@ class AssetManager { } ensureAsset( - documentId: Identifier | null | undefined, - asset: AssetLike | null | undefined, + documentId?: Identifier | null, + asset?: Nullable, { force = false, start = null, limit = null }: { force?: boolean; start?: number | null; limit?: number | null } = {}, - ): Promise { + ): Promise> { if (!documentId || !asset?.id) { - return Promise.resolve(asset || null); + return Promise.resolve(asset ?? null); } const requestedStart = Number.isInteger(start) && start > 0 ? start : 1; diff --git a/frontend/src/documents/DocumentSummarySection.tsx b/frontend/src/documents/DocumentSummarySection.tsx index 7d6410d..ef7509f 100644 --- a/frontend/src/documents/DocumentSummarySection.tsx +++ b/frontend/src/documents/DocumentSummarySection.tsx @@ -476,7 +476,7 @@ const DocumentSummarySection: React.FC = ({ }, [correspondents, document?.correspondents]); const metaRows = useMemo(() => { - const rows: { key: string; label: string; value: string | null | undefined }[] = []; + const rows: { key: string; label: string; value: string | null }[] = []; const currentVersionNumber = document?.current_version?.version_number; if (Number.isFinite(currentVersionNumber)) { rows.push({ diff --git a/frontend/src/documents/DocumentThumbnailImage.tsx b/frontend/src/documents/DocumentThumbnailImage.tsx index 38681a1..1e0679b 100644 --- a/frontend/src/documents/DocumentThumbnailImage.tsx +++ b/frontend/src/documents/DocumentThumbnailImage.tsx @@ -17,7 +17,7 @@ const DEFAULT_THUMBNAIL_SIZE = 48; // Detect when an element becomes visible within a scroll container so we can delay loading. const useLazyVisibility = ( rootRef: MutableRefObject | null, - resetKey: string | number | null | undefined, + resetKey?: string | number | null, ) => { const targetRef = useRef(null); const [isVisible, setIsVisible] = useState(false); @@ -66,10 +66,10 @@ const useLazyVisibility = ( return { ref: targetRef, isVisible }; }; -const getPageCount = (doc: DocumentLike | null | undefined) => - Number.isFinite(doc?.current_version?.metadata?.page_count) - ? (doc?.current_version?.metadata?.page_count as number) - : null; +const getPageCount = (doc?: DocumentLike | null) => { + const count = doc?.current_version?.metadata?.page_count; + return Number.isFinite(count) ? Number(count) : null; +}; type DocumentLike = AssetManagerDocumentLike; type AssetLike = AssetManagerAssetLike; diff --git a/frontend/src/documents/DocumentsGrid.tsx b/frontend/src/documents/DocumentsGrid.tsx index 30ab662..4def7a1 100644 --- a/frontend/src/documents/DocumentsGrid.tsx +++ b/frontend/src/documents/DocumentsGrid.tsx @@ -80,7 +80,7 @@ interface DocumentsGridProps { onTagClick?: (tagId?: Identifier | null) => void; scrollRef?: RefObject; onCorrespondentClick?: (correspondentId?: Identifier | null) => void; - activeCorrespondentIdSet?: Set | null; + activeCorrespondentIdSet?: Set | null; onDocumentRename?: (docId: Identifier, title: string) => Promise | boolean; } diff --git a/frontend/src/documents/DocumentsList.tsx b/frontend/src/documents/DocumentsList.tsx index 7bd09df..9898c29 100644 --- a/frontend/src/documents/DocumentsList.tsx +++ b/frontend/src/documents/DocumentsList.tsx @@ -84,7 +84,7 @@ export interface DocumentsListProps { tagLookupById?: Map | null; onTagClick?: (tagId?: Identifier | null) => void; onCorrespondentClick?: (correspondentId?: Identifier | null) => void; - activeCorrespondentIdSet?: Set | null; + activeCorrespondentIdSet?: Set | null; scrollRef?: RefObject; } diff --git a/frontend/src/documents/SelectionFloatingActions.tsx b/frontend/src/documents/SelectionFloatingActions.tsx index 7dd97a6..c0b9ea6 100644 --- a/frontend/src/documents/SelectionFloatingActions.tsx +++ b/frontend/src/documents/SelectionFloatingActions.tsx @@ -16,9 +16,9 @@ import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext'; const ROOT_FOLDER_LABEL = 'Documents'; type DocumentId = string | number; -type NullableDocumentId = DocumentId | null | undefined; +type NullableDocumentId = DocumentId | null; -type SelectedIdList = Array | null | undefined; +type SelectedIdList = NullableDocumentId[] | null; type FolderTreeNode = { id?: DocumentId; @@ -137,8 +137,8 @@ const buildFolderTreeOptions = (tree?: FolderTreeNode[] | null): SelectionAssign const buildTagAssignments = ( selectedDocuments: DocumentLike[], - tagLookupById: Map | null | undefined, - tags: TagOption[] | null | undefined, + tagLookupById: Map | null, + tags: TagOption[] | null, total: number, ): SelectionAssignmentMenuItem[] => { if (!total) { @@ -202,7 +202,7 @@ const buildTagAssignments = ( const buildCorrespondentAssignments = ( selectedDocuments: DocumentLike[], - correspondents: CorrespondentOption[] | null | undefined, + correspondents: CorrespondentOption[] | null, total: number, ): SelectionAssignmentMenuItem[] => { if (!total) { @@ -493,7 +493,7 @@ const SelectionFloatingActions: React.FC = ({ if (!documentIdList.length || !onMoveDocumentsToFolder) { return; } - const candidate = option as { id?: DocumentId; value?: DocumentId } | DocumentId | null | undefined; + const candidate = option as { id?: DocumentId; value?: DocumentId } | DocumentId | null; const value = isRecord(candidate) ? (candidate?.id ?? candidate?.value ?? null) : candidate; diff --git a/frontend/src/documents/documentMetadata.ts b/frontend/src/documents/documentMetadata.ts index c11b3a7..8f3a32c 100644 --- a/frontend/src/documents/documentMetadata.ts +++ b/frontend/src/documents/documentMetadata.ts @@ -20,7 +20,7 @@ export interface DocumentLike { export interface DocumentMetadataItem { label: string; - value: string | null | undefined; + value: string | null; } export const buildDocumentMetadataItems = (document?: DocumentLike | null): DocumentMetadataItem[] => { @@ -35,19 +35,19 @@ export const buildDocumentMetadataItems = (document?: DocumentLike | null): Docu { label: 'Updated at', value: formatDateTime(document.updated_at) }, { label: 'Filename', - value: document.filename, + value: document.filename ?? null, }, { label: 'Original filename', - value: document.original_name || '—', + value: document.original_name ?? null, }, { label: 'SHA-256 checksum', - value: metadata.checksum || '—', + value: metadata.checksum ?? null, }, { label: 'Content type', - value: document.content_type || '—', + value: document.content_type ?? null, }, ]; }; diff --git a/frontend/src/documents/documentSummary.ts b/frontend/src/documents/documentSummary.ts index 63414be..b9ba845 100644 --- a/frontend/src/documents/documentSummary.ts +++ b/frontend/src/documents/documentSummary.ts @@ -38,12 +38,12 @@ interface DescribeSummaryOptions { export interface DocumentSummaryRow { key: string; label: string; - value: string | null | undefined; + value: string | null; } export interface DocumentSummary { title: string | undefined; - originalName: string | null | undefined; + originalName: string | null; mimeTypeLabel: string; sizeLabel: string; createdAtLabel: string; @@ -51,7 +51,7 @@ export interface DocumentSummary { updatedAtLabel: string; pageCount: number | null; pageCountLabel: string; - folderLabel: string | null | undefined; + folderLabel: string | null; tags: TagEntry[]; correspondents: CorrespondentEntry[]; tagsSummary: string; @@ -59,7 +59,7 @@ export interface DocumentSummary { summaryRows: DocumentSummaryRow[]; } -const coercePageCount = (metadata: DocumentMetadata | null | undefined): number | null => { +const coercePageCount = (metadata?: DocumentMetadata | null): number | null => { const raw = metadata?.page_count; if (raw == null || raw === '') { return null; @@ -68,7 +68,7 @@ const coercePageCount = (metadata: DocumentMetadata | null | undefined): number return Number.isFinite(parsed) && parsed >= 0 ? parsed : null; }; -const sanitizeArray = (entries: (T | null | undefined)[] | null | undefined): T[] => +const sanitizeArray = (entries?: Array | null): T[] => Array.isArray(entries) ? entries.filter(Boolean) as T[] : []; export const describeDocumentSummary = (document?: SummaryDocument | null, options: DescribeSummaryOptions = {}): DocumentSummary => { @@ -79,7 +79,7 @@ export const describeDocumentSummary = (document?: SummaryDocument | null, optio if (!document) { return { title: '', - originalName: '', + originalName: null, mimeTypeLabel: '—', sizeLabel: '—', createdAtLabel: '—', @@ -110,7 +110,8 @@ export const describeDocumentSummary = (document?: SummaryDocument | null, optio const issuedLabel = formatDateTime(document.issued_at); const updatedAtLabel = formatDateTime(document.updated_at); - const folderLabel = document.folder_path; + const folderLabel = document.folder_path ?? null; + const displayFolderLabel = folderLabel ?? 'Documents'; const tags = sanitizeArray(document.tags); const correspondents = sanitizeArray(document.correspondents); @@ -130,7 +131,7 @@ export const describeDocumentSummary = (document?: SummaryDocument | null, optio { key: 'issued', label: 'Issued', value: issuedLabel }, { key: 'pages', label: 'Pages', value: pageCountLabel }, { key: 'updated', label: 'Updated', value: updatedAtLabel }, - { key: 'folder', label: 'Folder', value: folderLabel }, + { key: 'folder', label: 'Folder', value: displayFolderLabel }, { key: 'tags', label: 'Tags', value: tagsSummary }, { key: 'correspondents', label: 'Correspondents', value: correspondentsSummary }, ]; diff --git a/frontend/src/documents/tagTransfer.ts b/frontend/src/documents/tagTransfer.ts index be9813b..9555ebd 100644 --- a/frontend/src/documents/tagTransfer.ts +++ b/frontend/src/documents/tagTransfer.ts @@ -21,7 +21,7 @@ const serializePayload = (payload: TagPayload): string | null => { } }; -export const createTagTransferPayload = (tag: TagLike | null | undefined, sourceDocId: string | number | null = null): TagPayload | null => { +export const createTagTransferPayload = (tag?: TagLike | null, sourceDocId: string | number | null = null): TagPayload | null => { if (!tag || tag.id == null) { return null; } @@ -58,7 +58,7 @@ export const writeTagTransferData = (dataTransfer: DataTransfer | null, tag: Tag } }; -export const readTagTransferData = (dataTransfer: DataTransfer | null | undefined): string | null => { +export const readTagTransferData = (dataTransfer?: DataTransfer | null): string | null => { if (!dataTransfer) { return null; } diff --git a/frontend/src/documents/useEntryPointer.ts b/frontend/src/documents/useEntryPointer.ts index 6cd7a1f..7a89665 100644 --- a/frontend/src/documents/useEntryPointer.ts +++ b/frontend/src/documents/useEntryPointer.ts @@ -26,8 +26,8 @@ export interface WorkspaceEntry { } interface UseEntryPointerOptions { - resolveDocumentRowKey?: (id: string | number) => string | null | undefined; - resolveFolderRowKey?: (id: string | number) => string | null | undefined; + resolveDocumentRowKey?: (id: string | number) => string | null; + resolveFolderRowKey?: (id: string | number) => string | null; onSelectEntry?: (entry: WorkspaceEntry, event?: PointerEventLike | null, metadata?: EntryPointerMetadata) => void; onInspectDocument?: (id: string | number, metadata?: EntryPointerMetadata) => void; }