This commit is contained in:
2025-11-14 19:46:18 +01:00
parent a3c5494bf7
commit bd3eab8b40
10 changed files with 53 additions and 53 deletions
+21 -22
View File
@@ -2,6 +2,8 @@ import type { AxiosInstance } from 'axios';
export type Identifier = string | number; export type Identifier = string | number;
type Nullable<T> = T | null;
export interface AssetObject { export interface AssetObject {
ordinal?: number; ordinal?: number;
url?: string | null; url?: string | null;
@@ -40,12 +42,12 @@ export type EnsureAssetUrl = (
options?: { start?: number | null; limit?: number | null; force?: boolean; [key: string]: unknown }, options?: { start?: number | null; limit?: number | null; force?: boolean; [key: string]: unknown },
) => Promise<unknown>; ) => Promise<unknown>;
export type GetAsset = (document: DocumentLike, assetType: string) => AssetLike | null | undefined; export type GetAsset = (document: DocumentLike, assetType: string) => Nullable<AssetLike>;
export const getAssetFromGroup = ( export const getAssetFromGroup = (
assets: AssetLike[] | Record<string, AssetLike> | null | undefined, assets?: AssetLike[] | Record<string, AssetLike> | null,
assetType: string, assetType: string = '',
): AssetLike | null => { ): Nullable<AssetLike> => {
if (!assetType || !assets) { if (!assetType || !assets) {
return null; return null;
} }
@@ -57,11 +59,11 @@ export const getAssetFromGroup = (
return assets?.[assetType] || null; return assets?.[assetType] || null;
}; };
export const getAssetFromVersion = (currentVersion, assetType) => { export const getAssetFromVersion = (currentVersion: Nullable<DocumentVersionLike>, assetType: string) => {
if (!currentVersion) { if (!currentVersion) {
return null; return null;
} }
return getAssetFromGroup(currentVersion.assets, assetType); return getAssetFromGroup(currentVersion.assets ?? null, assetType);
}; };
const normalizeAssetObjects = (objects?: AssetObject[] | null): AssetObject[] => { 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 createAssetView = (asset?: AssetLike | null): AssetView => new AssetView(asset);
export const resolveDocumentAssetUrl = ( export const resolveDocumentAssetUrl = (
doc: DocumentLike | null | undefined, doc: Nullable<DocumentLike>,
type: string, type: string,
{ {
ensureAssetUrl, ensureAssetUrl,
@@ -195,7 +197,7 @@ export const resolveDocumentAssetUrl = (
ensureOptions?: { start?: number; limit?: number; [key: string]: unknown }; ensureOptions?: { start?: number; limit?: number; [key: string]: unknown };
objectOrdinal?: number; objectOrdinal?: number;
} = {}, } = {},
): string | null => { ): Nullable<string> => {
if (!doc || !type) { if (!doc || !type) {
return null; return null;
} }
@@ -251,13 +253,13 @@ class AssetManager {
this.api = api; this.api = api;
} }
rememberAsset(entry: AssetLike | null | undefined) { rememberAsset(entry?: Nullable<AssetLike>) {
if (entry?.id) { if (entry?.id) {
this.assetCache.set(entry.id, entry); this.assetCache.set(entry.id, entry);
} }
} }
hydrateAsset(asset: AssetLike | null | undefined): AssetLike | null | undefined { hydrateAsset(asset?: Nullable<AssetLike>): Nullable<AssetLike> {
if (!asset || !asset.id) { if (!asset || !asset.id) {
return asset; return asset;
} }
@@ -287,7 +289,7 @@ class AssetManager {
return merged; return merged;
} }
hydrateDocument(document: DocumentLike | null | undefined): DocumentLike | null | undefined { hydrateDocument(document?: Nullable<DocumentLike>): Nullable<DocumentLike> {
if (!document) { if (!document) {
return document; return document;
} }
@@ -330,14 +332,14 @@ class AssetManager {
return { ...document, current_version: nextCurrentVersion }; return { ...document, current_version: nextCurrentVersion };
} }
hydrateDocuments(documents: DocumentLike[] | null | undefined) { hydrateDocuments(documents?: DocumentLike[] | null) {
if (!Array.isArray(documents)) { if (!Array.isArray(documents)) {
return documents; return documents ?? [];
} }
return documents.map((doc) => this.hydrateDocument(doc)); return documents.map((doc) => this.hydrateDocument(doc));
} }
hydrateDetail(detail: { document?: DocumentLike; assets?: AssetLike[] } | null | undefined) { hydrateDetail(detail?: { document?: DocumentLike; assets?: AssetLike[] } | null) {
if (!detail) { if (!detail) {
return detail; return detail;
} }
@@ -366,10 +368,7 @@ class AssetManager {
return changed ? next : detail; return changed ? next : detail;
} }
hydrateFolderContents(contents: { hydrateFolderContents(contents?: { documents?: DocumentLike[]; document?: DocumentLike } | null) {
documents?: DocumentLike[];
document?: DocumentLike;
} | null | undefined) {
if (!contents) { if (!contents) {
return contents; return contents;
} }
@@ -384,12 +383,12 @@ class AssetManager {
} }
ensureAsset( ensureAsset(
documentId: Identifier | null | undefined, documentId?: Identifier | null,
asset: AssetLike | null | undefined, asset?: Nullable<AssetLike>,
{ force = false, start = null, limit = null }: { force?: boolean; start?: number | null; limit?: number | null } = {}, { force = false, start = null, limit = null }: { force?: boolean; start?: number | null; limit?: number | null } = {},
): Promise<AssetLike | null> { ): Promise<Nullable<AssetLike>> {
if (!documentId || !asset?.id) { if (!documentId || !asset?.id) {
return Promise.resolve(asset || null); return Promise.resolve(asset ?? null);
} }
const requestedStart = Number.isInteger(start) && start > 0 ? start : 1; const requestedStart = Number.isInteger(start) && start > 0 ? start : 1;
@@ -476,7 +476,7 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
}, [correspondents, document?.correspondents]); }, [correspondents, document?.correspondents]);
const metaRows = useMemo(() => { 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; const currentVersionNumber = document?.current_version?.version_number;
if (Number.isFinite(currentVersionNumber)) { if (Number.isFinite(currentVersionNumber)) {
rows.push({ rows.push({
@@ -17,7 +17,7 @@ const DEFAULT_THUMBNAIL_SIZE = 48;
// Detect when an element becomes visible within a scroll container so we can delay loading. // Detect when an element becomes visible within a scroll container so we can delay loading.
const useLazyVisibility = ( const useLazyVisibility = (
rootRef: MutableRefObject<Element | null> | null, rootRef: MutableRefObject<Element | null> | null,
resetKey: string | number | null | undefined, resetKey?: string | number | null,
) => { ) => {
const targetRef = useRef<HTMLDivElement | null>(null); const targetRef = useRef<HTMLDivElement | null>(null);
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
@@ -66,10 +66,10 @@ const useLazyVisibility = (
return { ref: targetRef, isVisible }; return { ref: targetRef, isVisible };
}; };
const getPageCount = (doc: DocumentLike | null | undefined) => const getPageCount = (doc?: DocumentLike | null) => {
Number.isFinite(doc?.current_version?.metadata?.page_count) const count = doc?.current_version?.metadata?.page_count;
? (doc?.current_version?.metadata?.page_count as number) return Number.isFinite(count) ? Number(count) : null;
: null; };
type DocumentLike = AssetManagerDocumentLike; type DocumentLike = AssetManagerDocumentLike;
type AssetLike = AssetManagerAssetLike; type AssetLike = AssetManagerAssetLike;
+1 -1
View File
@@ -80,7 +80,7 @@ interface DocumentsGridProps {
onTagClick?: (tagId?: Identifier | null) => void; onTagClick?: (tagId?: Identifier | null) => void;
scrollRef?: RefObject<HTMLElement | null>; scrollRef?: RefObject<HTMLElement | null>;
onCorrespondentClick?: (correspondentId?: Identifier | null) => void; onCorrespondentClick?: (correspondentId?: Identifier | null) => void;
activeCorrespondentIdSet?: Set<Identifier | null | undefined> | null; activeCorrespondentIdSet?: Set<Identifier | null> | null;
onDocumentRename?: (docId: Identifier, title: string) => Promise<boolean> | boolean; onDocumentRename?: (docId: Identifier, title: string) => Promise<boolean> | boolean;
} }
+1 -1
View File
@@ -84,7 +84,7 @@ export interface DocumentsListProps {
tagLookupById?: Map<Identifier, DocumentTag> | null; tagLookupById?: Map<Identifier, DocumentTag> | null;
onTagClick?: (tagId?: Identifier | null) => void; onTagClick?: (tagId?: Identifier | null) => void;
onCorrespondentClick?: (correspondentId?: Identifier | null) => void; onCorrespondentClick?: (correspondentId?: Identifier | null) => void;
activeCorrespondentIdSet?: Set<Identifier | null | undefined> | null; activeCorrespondentIdSet?: Set<Identifier | null> | null;
scrollRef?: RefObject<HTMLElement | null>; scrollRef?: RefObject<HTMLElement | null>;
} }
@@ -16,9 +16,9 @@ import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
const ROOT_FOLDER_LABEL = 'Documents'; const ROOT_FOLDER_LABEL = 'Documents';
type DocumentId = string | number; type DocumentId = string | number;
type NullableDocumentId = DocumentId | null | undefined; type NullableDocumentId = DocumentId | null;
type SelectedIdList = Array<NullableDocumentId> | null | undefined; type SelectedIdList = NullableDocumentId[] | null;
type FolderTreeNode = { type FolderTreeNode = {
id?: DocumentId; id?: DocumentId;
@@ -137,8 +137,8 @@ const buildFolderTreeOptions = (tree?: FolderTreeNode[] | null): SelectionAssign
const buildTagAssignments = ( const buildTagAssignments = (
selectedDocuments: DocumentLike[], selectedDocuments: DocumentLike[],
tagLookupById: Map<DocumentId, TagOption> | null | undefined, tagLookupById: Map<DocumentId, TagOption> | null,
tags: TagOption[] | null | undefined, tags: TagOption[] | null,
total: number, total: number,
): SelectionAssignmentMenuItem[] => { ): SelectionAssignmentMenuItem[] => {
if (!total) { if (!total) {
@@ -202,7 +202,7 @@ const buildTagAssignments = (
const buildCorrespondentAssignments = ( const buildCorrespondentAssignments = (
selectedDocuments: DocumentLike[], selectedDocuments: DocumentLike[],
correspondents: CorrespondentOption[] | null | undefined, correspondents: CorrespondentOption[] | null,
total: number, total: number,
): SelectionAssignmentMenuItem[] => { ): SelectionAssignmentMenuItem[] => {
if (!total) { if (!total) {
@@ -493,7 +493,7 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
if (!documentIdList.length || !onMoveDocumentsToFolder) { if (!documentIdList.length || !onMoveDocumentsToFolder) {
return; 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) const value = isRecord(candidate)
? (candidate?.id ?? candidate?.value ?? null) ? (candidate?.id ?? candidate?.value ?? null)
: candidate; : candidate;
+5 -5
View File
@@ -20,7 +20,7 @@ export interface DocumentLike {
export interface DocumentMetadataItem { export interface DocumentMetadataItem {
label: string; label: string;
value: string | null | undefined; value: string | null;
} }
export const buildDocumentMetadataItems = (document?: DocumentLike | null): DocumentMetadataItem[] => { 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: 'Updated at', value: formatDateTime(document.updated_at) },
{ {
label: 'Filename', label: 'Filename',
value: document.filename, value: document.filename ?? null,
}, },
{ {
label: 'Original filename', label: 'Original filename',
value: document.original_name || '—', value: document.original_name ?? null,
}, },
{ {
label: 'SHA-256 checksum', label: 'SHA-256 checksum',
value: metadata.checksum || '—', value: metadata.checksum ?? null,
}, },
{ {
label: 'Content type', label: 'Content type',
value: document.content_type || '—', value: document.content_type ?? null,
}, },
]; ];
}; };
+9 -8
View File
@@ -38,12 +38,12 @@ interface DescribeSummaryOptions {
export interface DocumentSummaryRow { export interface DocumentSummaryRow {
key: string; key: string;
label: string; label: string;
value: string | null | undefined; value: string | null;
} }
export interface DocumentSummary { export interface DocumentSummary {
title: string | undefined; title: string | undefined;
originalName: string | null | undefined; originalName: string | null;
mimeTypeLabel: string; mimeTypeLabel: string;
sizeLabel: string; sizeLabel: string;
createdAtLabel: string; createdAtLabel: string;
@@ -51,7 +51,7 @@ export interface DocumentSummary {
updatedAtLabel: string; updatedAtLabel: string;
pageCount: number | null; pageCount: number | null;
pageCountLabel: string; pageCountLabel: string;
folderLabel: string | null | undefined; folderLabel: string | null;
tags: TagEntry[]; tags: TagEntry[];
correspondents: CorrespondentEntry[]; correspondents: CorrespondentEntry[];
tagsSummary: string; tagsSummary: string;
@@ -59,7 +59,7 @@ export interface DocumentSummary {
summaryRows: DocumentSummaryRow[]; summaryRows: DocumentSummaryRow[];
} }
const coercePageCount = (metadata: DocumentMetadata | null | undefined): number | null => { const coercePageCount = (metadata?: DocumentMetadata | null): number | null => {
const raw = metadata?.page_count; const raw = metadata?.page_count;
if (raw == null || raw === '') { if (raw == null || raw === '') {
return null; return null;
@@ -68,7 +68,7 @@ const coercePageCount = (metadata: DocumentMetadata | null | undefined): number
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null; return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
}; };
const sanitizeArray = <T>(entries: (T | null | undefined)[] | null | undefined): T[] => const sanitizeArray = <T>(entries?: Array<T | null> | null): T[] =>
Array.isArray(entries) ? entries.filter(Boolean) as T[] : []; Array.isArray(entries) ? entries.filter(Boolean) as T[] : [];
export const describeDocumentSummary = (document?: SummaryDocument | null, options: DescribeSummaryOptions = {}): DocumentSummary => { export const describeDocumentSummary = (document?: SummaryDocument | null, options: DescribeSummaryOptions = {}): DocumentSummary => {
@@ -79,7 +79,7 @@ export const describeDocumentSummary = (document?: SummaryDocument | null, optio
if (!document) { if (!document) {
return { return {
title: '', title: '',
originalName: '', originalName: null,
mimeTypeLabel: '—', mimeTypeLabel: '—',
sizeLabel: '—', sizeLabel: '—',
createdAtLabel: '—', createdAtLabel: '—',
@@ -110,7 +110,8 @@ export const describeDocumentSummary = (document?: SummaryDocument | null, optio
const issuedLabel = formatDateTime(document.issued_at); const issuedLabel = formatDateTime(document.issued_at);
const updatedAtLabel = formatDateTime(document.updated_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<TagEntry>(document.tags); const tags = sanitizeArray<TagEntry>(document.tags);
const correspondents = sanitizeArray<CorrespondentEntry>(document.correspondents); const correspondents = sanitizeArray<CorrespondentEntry>(document.correspondents);
@@ -130,7 +131,7 @@ export const describeDocumentSummary = (document?: SummaryDocument | null, optio
{ key: 'issued', label: 'Issued', value: issuedLabel }, { key: 'issued', label: 'Issued', value: issuedLabel },
{ key: 'pages', label: 'Pages', value: pageCountLabel }, { key: 'pages', label: 'Pages', value: pageCountLabel },
{ key: 'updated', label: 'Updated', value: updatedAtLabel }, { 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: 'tags', label: 'Tags', value: tagsSummary },
{ key: 'correspondents', label: 'Correspondents', value: correspondentsSummary }, { key: 'correspondents', label: 'Correspondents', value: correspondentsSummary },
]; ];
+2 -2
View File
@@ -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) { if (!tag || tag.id == null) {
return 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) { if (!dataTransfer) {
return null; return null;
} }
+2 -2
View File
@@ -26,8 +26,8 @@ export interface WorkspaceEntry {
} }
interface UseEntryPointerOptions { interface UseEntryPointerOptions {
resolveDocumentRowKey?: (id: string | number) => string | null | undefined; resolveDocumentRowKey?: (id: string | number) => string | null;
resolveFolderRowKey?: (id: string | number) => string | null | undefined; resolveFolderRowKey?: (id: string | number) => string | null;
onSelectEntry?: (entry: WorkspaceEntry, event?: PointerEventLike | null, metadata?: EntryPointerMetadata) => void; onSelectEntry?: (entry: WorkspaceEntry, event?: PointerEventLike | null, metadata?: EntryPointerMetadata) => void;
onInspectDocument?: (id: string | number, metadata?: EntryPointerMetadata) => void; onInspectDocument?: (id: string | number, metadata?: EntryPointerMetadata) => void;
} }