diff --git a/frontend/src/app/appLayoutUtils.ts b/frontend/src/app/appLayoutUtils.ts index 362639c..270ac90 100644 --- a/frontend/src/app/appLayoutUtils.ts +++ b/frontend/src/app/appLayoutUtils.ts @@ -16,7 +16,7 @@ export const resolveApiPath = (path = '') => path; const makeRowKey = (type, id) => id ? `${type}${ROW_KEY_SEPARATOR}${id}` : `${type}${ROW_KEY_SEPARATOR}`; -const normalizeRowKey = (key: string | number | null | undefined) => String(key ?? ''); +const normalizeRowKey = (key: string | number | null) => String(key ?? ''); const getRowType = (key) => normalizeRowKey(key).split(ROW_KEY_SEPARATOR, 1)[0] ?? ''; diff --git a/frontend/src/app/useDocumentPreview.ts b/frontend/src/app/useDocumentPreview.ts index 3d119c5..2e4bb23 100644 --- a/frontend/src/app/useDocumentPreview.ts +++ b/frontend/src/app/useDocumentPreview.ts @@ -25,11 +25,11 @@ type PreviewEntry = { canGoNext?: boolean; goPrev?: () => void; goNext?: () => void; -} | null; +}; interface AssetManagerLike { - hydrateDetail: (payload: unknown) => { document?: DocumentLike } | null | undefined; - hydrateDocument: (payload: unknown) => DocumentLike | null | undefined; + hydrateDetail: (payload: unknown) => { document?: DocumentLike } | null; + hydrateDocument: (payload: unknown) => DocumentLike | null; } interface ApiClient { @@ -60,10 +60,10 @@ interface UseDocumentPreviewArgs { interface UseDocumentPreviewResult { previewEntries: Map; previewDocuments: Map; - ensurePreviewUrl: (documentId: DocumentId | null, options?: { force?: boolean }) => Promise; - ensurePreviewData: (documentId: DocumentId | null) => Promise; - openDocumentPreview: (documentId: DocumentId | null, options?: { replace?: boolean }) => void; - closeDocumentPreview: (folderId?: FolderId | null) => void; + ensurePreviewUrl: (documentId: DocumentId, options?: { force?: boolean }) => Promise; + ensurePreviewData: (documentId: DocumentId) => Promise; + openDocumentPreview: (documentId: DocumentId, options?: { replace?: boolean }) => void; + closeDocumentPreview: (folderId?: FolderId) => void; resetPreviewState: () => void; removePreviewEntries: (ids: DocumentId[]) => void; } @@ -114,7 +114,7 @@ const useDocumentPreview = ({ }); }, []); - const cachePreviewDocument = useCallback((doc: DocumentLike | null | undefined) => { + const cachePreviewDocument = useCallback((doc: DocumentLike) => { if (!doc?.id) { return; } @@ -129,7 +129,7 @@ const useDocumentPreview = ({ }); }, []); - const removeCachedPreviewDocument = useCallback((documentId?: DocumentId | null) => { + const removeCachedPreviewDocument = useCallback((documentId: DocumentId) => { if (!documentId) { return; } @@ -144,7 +144,7 @@ const useDocumentPreview = ({ }, []); const ensurePreviewUrl = useCallback( - async (documentId: DocumentId | null, { force = false }: { force?: boolean } = {}): Promise => { + async (documentId: DocumentId, { force = false }: { force?: boolean } = {}): Promise => { if (!documentId) return null; const existing = previewEntries.get(documentId) || null; @@ -194,7 +194,7 @@ const useDocumentPreview = ({ ); const ensurePreviewData = useCallback( - async (documentId: DocumentId | null): Promise => { + async (documentId: DocumentId): Promise => { if (!documentId) return null; const findInCache = () => { @@ -248,7 +248,7 @@ const useDocumentPreview = ({ ); const openDocumentPreview = useCallback( - (documentId: DocumentId | null, { replace = false }: { replace?: boolean } = {}) => { + (documentId: DocumentId, { replace = false }: { replace?: boolean } = {}) => { if (!documentId) return; detailPanelControlRef.current?.close?.(); previewReturnPathRef.current = `${locationPathname}${locationSearch}`; @@ -258,7 +258,7 @@ const useDocumentPreview = ({ ); const closeDocumentPreview = useCallback( - (folderId: FolderId | null = null) => { + (folderId?: FolderId) => { const fallbackPath = previewReturnPathRef.current; previewReturnPathRef.current = null; diff --git a/frontend/src/app/useDocumentSelection.ts b/frontend/src/app/useDocumentSelection.ts index d5a3809..36a4077 100644 --- a/frontend/src/app/useDocumentSelection.ts +++ b/frontend/src/app/useDocumentSelection.ts @@ -11,11 +11,11 @@ interface SelectionEventLike { } interface UseDocumentSelectionOptions { - resolveDocumentRowKey: (id: DocumentId | null | undefined) => RowKey | null | undefined; - resolveFolderRowKey: (id: DocumentId | null | undefined) => RowKey | null | undefined; + resolveDocumentRowKey: (id: DocumentId | null) => RowKey | null; + resolveFolderRowKey: (id: DocumentId | null) => RowKey | null; isDocumentRowKey: (key?: RowKey | null) => boolean; isFolderRowKey: (key?: RowKey | null) => boolean; - getRowId: (key?: RowKey | null) => DocumentId | null | undefined; + getRowId: (key?: RowKey | null) => DocumentId | null; initialEntries?: RowKey[]; } @@ -93,7 +93,7 @@ export const useDocumentSelection = ({ const applySelection = useCallback( ( - rowKeys: Array, + rowKeys: Array, { anchor, interactedKeys = [] }: ApplySelectionOptions = {}, ) => { const visibleRowKeySet = visibleRowKeySetRef.current; @@ -101,7 +101,7 @@ export const useDocumentSelection = ({ (rowKeys || []).forEach((key) => { if (!key) return; - let canonicalKey: RowKey | null | undefined = null; + let canonicalKey: RowKey | null = null; if (visibleRowKeySet.has(key)) { canonicalKey = key; } else if (isDocumentRowKey(key)) { @@ -174,7 +174,7 @@ export const useDocumentSelection = ({ }, [applySelection]); const handleEntrySelection = useCallback( - (rowKey: RowKey | null | undefined, event?: SelectionEventLike) => { + (rowKey: RowKey | null, event?: SelectionEventLike) => { const visibleRowKeySet = visibleRowKeySetRef.current; const navigableRowKeys = navigableRowKeysRef.current; if (!rowKey || !visibleRowKeySet.has(rowKey)) { diff --git a/frontend/src/app/useWorkspaceSelection.ts b/frontend/src/app/useWorkspaceSelection.ts index fe22d7b..c2b13a1 100644 --- a/frontend/src/app/useWorkspaceSelection.ts +++ b/frontend/src/app/useWorkspaceSelection.ts @@ -9,8 +9,8 @@ interface SelectionEntry { } interface WorkspaceSelectionOptions { - resolveDocumentRowKey?: (id: string | number) => RowKey | null | undefined; - resolveFolderRowKey?: (id: string | number) => RowKey | null | undefined; + resolveDocumentRowKey?: (id: string | number) => RowKey | null; + resolveFolderRowKey?: (id: string | number) => RowKey | null; isDocumentRowKey?: (key: RowKey | SelectionEntry) => boolean; isFolderRowKey?: (key: RowKey | SelectionEntry) => boolean; getRowId?: (key: RowKey | SelectionEntry) => string | number | null; diff --git a/frontend/src/desktop/DesktopPreviewCard.tsx b/frontend/src/desktop/DesktopPreviewCard.tsx index f8a595e..1ed7491 100644 --- a/frontend/src/desktop/DesktopPreviewCard.tsx +++ b/frontend/src/desktop/DesktopPreviewCard.tsx @@ -27,7 +27,7 @@ type EnsureAssetUrl = ( options?: { start?: number; limit?: number; [key: string]: unknown }, ) => Promise; -type GetDocumentAsset = (document: DocumentLike | null | undefined, assetType: string) => AssetLike | null | undefined; +type GetDocumentAsset = (document: DocumentLike | null, assetType: string) => AssetLike | null; interface NavigatorSnapshot { url: string | null; diff --git a/frontend/src/desktop/DesktopWorkspace.tsx b/frontend/src/desktop/DesktopWorkspace.tsx index 9bc6ebf..f78fb15 100644 --- a/frontend/src/desktop/DesktopWorkspace.tsx +++ b/frontend/src/desktop/DesktopWorkspace.tsx @@ -30,7 +30,7 @@ import '../styles/workspace/workspace-cards.css'; type Identifier = string | number; -type TagLike = { id?: Identifier | null; label?: string; color?: string | null } | null | undefined; +type TagLike = { id?: Identifier | null; label?: string; color?: string | null } | null; export interface DeskDocument { id?: Identifier | null; @@ -126,7 +126,7 @@ interface DesktopWorkspaceProps { onAssignTagToDocument?: (...args: unknown[]) => void; ensureAssetUrl?: (...args: unknown[]) => Promise | unknown; getDocumentAsset?: (...args: unknown[]) => unknown; - activeTagIds?: Array; + activeTagIds?: Array; selectedDocumentIds?: Identifier[]; onClearSelection?: () => void; detailPanelOpen?: boolean; @@ -142,7 +142,7 @@ interface DesktopWorkspaceViewProps { handleCanvasDragOver: (event: React.DragEvent) => void; handleCanvasDragLeave: (event: React.DragEvent) => void; handleCanvasDrop: (event: React.DragEvent) => void; - ensureDocumentSize: (doc: DeskDocument | null | undefined) => DocumentSizeInfo | null; + ensureDocumentSize: (doc: DeskDocument | null) => DocumentSizeInfo | null; layoutSnapshot: Map; layoutRef: React.MutableRefObject>; dragTransformsRef: React.MutableRefObject>; @@ -176,15 +176,15 @@ interface DesktopWorkspaceViewProps { detailPanelOpen: boolean; onCloseDetailPanel?: DesktopWorkspaceProps['onCloseDetailPanel']; documentLookup: Map; - resolveBaseMetrics: (doc: DeskDocument | null | undefined, cardWidth: number, cardHeight: number) => { + resolveBaseMetrics: (doc: DeskDocument | null, cardWidth: number, cardHeight: number) => { baseWidth: number; baseHeight: number; baseScale: number; }; - bringToFront: (docId: Identifier | null | undefined) => void; + bringToFront: (docId: Identifier | null) => void; setDraggingId: (value: string | null) => void; canvasSize: { width: number; height: number }; - openOverlayForDoc: (docId: Identifier | null | undefined, originInfo?: OverlayOriginHint | null) => void; + openOverlayForDoc: (docId: Identifier | null, originInfo?: OverlayOriginHint | null) => void; recalcVisibleDocIds: () => void; dragSettings: DragSettings; onInspectDocument?: DesktopWorkspaceProps['onInspectDocument']; @@ -231,7 +231,7 @@ const DesktopWorkspace: React.FC = ({ ); const [docSizeVersion, setDocSizeVersion] = useState(0); const docSizeMapRef = useRef>(new Map()); - const ensureDocumentSize = useCallback((doc: DeskDocument | null | undefined): DocumentSizeInfo | null => { + const ensureDocumentSize = useCallback((doc: DeskDocument | null): DocumentSizeInfo | null => { if (!doc?.id) { return null; } @@ -312,7 +312,7 @@ const DesktopWorkspace: React.FC = ({ const layoutRef = useRef>(layoutSnapshot); layoutRef.current = engine.layout as Map; - const bringToFront = useCallback((docId: Identifier | null | undefined) => { + const bringToFront = useCallback((docId: Identifier | null) => { engine.bringToFront(docId); }, [engine]); @@ -425,7 +425,7 @@ const DesktopWorkspace: React.FC = ({ }, [engine]); const resolvePreviewDimensions = useCallback( - (doc: DeskDocument | null | undefined): PreviewMetadataEntry | null => { + (doc: DeskDocument | null): PreviewMetadataEntry | null => { if (!doc?.id) { return null; } @@ -581,7 +581,7 @@ const DesktopWorkspace: React.FC = ({ }, [overlayDocId, documentLookup]); const resolveBaseMetrics = useCallback( - (doc: DeskDocument | null | undefined, cardWidth: number, cardHeight: number) => { + (doc: DeskDocument | null, cardWidth: number, cardHeight: number) => { const previewDims = doc ? resolvePreviewDimensions(doc) : null; if (previewDims?.width && previewDims?.height) { const baseWidth = Math.max(previewDims.width, cardWidth); @@ -612,7 +612,7 @@ const DesktopWorkspace: React.FC = ({ }, [draggingId, items, setDraggingId]); const openOverlayForDoc = useCallback( - (docId: Identifier | null | undefined, originInfo: OverlayOriginHint | null = null) => { + (docId: Identifier | null, originInfo: OverlayOriginHint | null = null) => { if (!docId) { return; } diff --git a/frontend/src/desktop/events.ts b/frontend/src/desktop/events.ts index 25419cb..5aa7eeb 100644 --- a/frontend/src/desktop/events.ts +++ b/frontend/src/desktop/events.ts @@ -22,7 +22,7 @@ export const preventAll = (event?: PreventableEvent | null): void => { type AnyFn = (...args: unknown[]) => unknown; export const safeInvoke = ( - fn: Fn | null | undefined, + fn: Fn | null, ...args: Parameters ): ReturnType | undefined => (fn ? (fn(...args) as ReturnType) : undefined); diff --git a/frontend/src/desktop/hooks/usePreviewMetadata.ts b/frontend/src/desktop/hooks/usePreviewMetadata.ts index 9ea4939..e03fbd1 100644 --- a/frontend/src/desktop/hooks/usePreviewMetadata.ts +++ b/frontend/src/desktop/hooks/usePreviewMetadata.ts @@ -18,11 +18,11 @@ interface PreviewMetadataEntry { height: number; } -type GetDocumentAsset = (doc: DocumentLike, type: string) => AssetLike | null | undefined; -type EnsureAssetUrl = (docId: string | number, asset: AssetLike, options?: { start?: number; limit?: number }) => Promise; +type GetDocumentAsset = (doc: DocumentLike, type: string) => AssetLike | null; +type EnsureAssetUrl = (docId: string | number, asset: AssetLike, options?: { start?: number; limit?: number }) => Promise; const usePreviewMetadata = ( - documents: DocumentLike[] | null | undefined, + documents: DocumentLike[] | null, getDocumentAsset?: GetDocumentAsset, ensureAssetUrl?: EnsureAssetUrl, ) => { @@ -50,7 +50,7 @@ const usePreviewMetadata = ( let view = createAssetView(asset); let metadata = view.getPrimaryMetadata(); - const hasDimensions = (meta: { width?: number | string; height?: number | string } | null | undefined) => + const hasDimensions = (meta: { width?: number | string; height?: number | string } | null) => Number.isFinite(Number(meta?.width)) && Number.isFinite(Number(meta?.height)) && Number(meta.width) > 0 && diff --git a/frontend/src/desktop/useDeskWorkspaceProps.ts b/frontend/src/desktop/useDeskWorkspaceProps.ts index d9686fb..4520749 100644 --- a/frontend/src/desktop/useDeskWorkspaceProps.ts +++ b/frontend/src/desktop/useDeskWorkspaceProps.ts @@ -3,7 +3,7 @@ import { useCallback, useMemo } from 'react'; type Identifier = string | number; type DocumentEntry = { id?: Identifier } & Record; -type InspectTarget = DocumentEntry | Identifier | null | undefined; +type InspectTarget = DocumentEntry | Identifier | null; type WorkspaceViewMode = 'desk' | 'grid' | 'list' | string; diff --git a/frontend/src/desktop/useDocumentDrag.ts b/frontend/src/desktop/useDocumentDrag.ts index de07768..a356f70 100644 --- a/frontend/src/desktop/useDocumentDrag.ts +++ b/frontend/src/desktop/useDocumentDrag.ts @@ -59,10 +59,10 @@ interface DragGroupItemInternal extends EngineGroupItem { initialRotation?: number; } -type EnsureDocumentSizeFn = (doc: DocumentLike | null | undefined) => DocumentSizeInfo | null; +type EnsureDocumentSizeFn = (doc: DocumentLike | null) => DocumentSizeInfo | null; type ResolveBaseMetricsFn = ( - doc: DocumentLike | null | undefined, + doc: DocumentLike | null, width: number, height: number, ) => { baseWidth: number; baseHeight: number; baseScale: number }; @@ -75,7 +75,7 @@ interface DragSettings { } interface PointerDownOptions { - stackDocIds?: Array; + stackDocIds?: Array; stackSelectionApplied?: boolean; wasSelected?: boolean; modifierActive?: boolean; @@ -90,23 +90,23 @@ interface UseDocumentDragOptions { documentLookup: Map; ensureDocumentSize: EnsureDocumentSizeFn; resolveBaseMetrics: ResolveBaseMetricsFn; - bringToFront: (docId: Identifier | null | undefined) => void; + bringToFront: (docId: Identifier | null) => void; setDraggingId: (docKey: string | null) => void; canvasSize: { width: number; height: number }; openOverlayForDoc?: ( - docId: Identifier | null | undefined, + docId: Identifier | null, originInfo?: { rotation: number; scale: number; width: number; height: number }, ) => void; recalcVisibleDocIds: () => void; settings?: DragSettings; containerRef?: RefObject; - onInspectDocument?: (docId: Identifier | null | undefined, event?: PointerEvent | ReactPointerEvent) => void; + onInspectDocument?: (docId: Identifier | null, event?: PointerEvent | ReactPointerEvent) => void; onDocumentStackSelect?: ( docIds: Identifier[], event: PointerEvent | ReactPointerEvent, options?: { replace?: boolean }, ) => void; - selectedDocumentIds?: Array; + selectedDocumentIds?: Array; markLayoutDirty?: () => void; } @@ -225,7 +225,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { }); const dragStateRef = useRef(null); - const setDragTransform = useCallback((docKey: Identifier | null | undefined, transform: DragTransform | null) => { + const setDragTransform = useCallback((docKey: Identifier | null, transform: DragTransform | null) => { if (!docKey) { return; } @@ -248,7 +248,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { map.clear(); }, [dragTransformsRef]); - const commitActiveDragTransforms = useCallback((docIds: Array | null = null) => { + const commitActiveDragTransforms = useCallback((docIds: Array | null = null) => { const map = dragTransformsRef?.current; if (!map || !map.size) { return; diff --git a/frontend/src/desktop/workspaceEngine.ts b/frontend/src/desktop/workspaceEngine.ts index abb85ae..24a75a4 100644 --- a/frontend/src/desktop/workspaceEngine.ts +++ b/frontend/src/desktop/workspaceEngine.ts @@ -107,7 +107,7 @@ type WorkspaceSubscriber = () => void; type DeskDocument = { id?: string | number | null } & Record; -type EnsureDocumentSize = (doc: DeskDocument) => DocumentSize | null | undefined; +type EnsureDocumentSize = (doc: DeskDocument) => DocumentSize | null; type ResolveBaseMetrics = () => BaseMetrics; @@ -144,7 +144,7 @@ export const TORQUE_TO_ACCELERATION = 0.006; export const SETTLE_ANGULAR_VELOCITY = 1.2; export const applyDomTransform = ( - node: HTMLElement | null | undefined, + node: HTMLElement | null, { centerX, centerY, @@ -662,7 +662,7 @@ export class WorkspaceEngine { } } - setItems(items: DeskDocument[] | null | undefined): void { + setItems(items: DeskDocument[] | null): void { const normalized = Array.isArray(items) ? items : []; this.items = normalized; const canGenerateLayoutImmediately = @@ -689,7 +689,7 @@ export class WorkspaceEngine { this.resolveBaseMetrics = fn; } - setItemRefs(ref: ItemRefs | null | undefined): void { + setItemRefs(ref: ItemRefs | null): void { this.itemRefs = ref || { current: new Map() }; } @@ -778,7 +778,7 @@ export class WorkspaceEngine { updateLayoutEntry( docId: string | number | null, - updater: (previous: LayoutEntry | null) => LayoutEntry | null | undefined, + updater: (previous: LayoutEntry | null) => LayoutEntry | null, ): void { if (docId == null) { return; diff --git a/frontend/src/detail/useDetailWorkspace.ts b/frontend/src/detail/useDetailWorkspace.ts index d4fb063..caa1882 100644 --- a/frontend/src/detail/useDetailWorkspace.ts +++ b/frontend/src/detail/useDetailWorkspace.ts @@ -54,7 +54,7 @@ interface UseDetailWorkspaceArgs { handleTagRemove?: (...args: unknown[]) => void; ensureAssetUrl?: EnsureAssetUrl; getDocumentAsset?: GetDocumentAsset; - ensurePreviewData?: (docId: Identifier, options?: Record) => Promise; + ensurePreviewData?: (docId: Identifier, options?: Record) => Promise; correspondents?: unknown[]; handleCorrespondentAdd?: (...args: unknown[]) => void; handleCorrespondentRemove?: (...args: unknown[]) => void; diff --git a/frontend/src/documents/DocumentInfoPanel.tsx b/frontend/src/documents/DocumentInfoPanel.tsx index 4e19842..7acfa1b 100644 --- a/frontend/src/documents/DocumentInfoPanel.tsx +++ b/frontend/src/documents/DocumentInfoPanel.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import type { ReactNode } from 'react'; import DocumentSummarySection, { DocumentSummarySectionProps } from './DocumentSummarySection'; -import { buildDocumentMetadataItems, extractDocumentMetadataPayload } from './documentMetadata'; +import { describeDocumentSummary, extractDocumentMetadataPayload, type DocumentSummaryRow } from './documentSummary'; type PanelTab = { id: string; label: string; render: (context?: Record) => ReactNode }; @@ -15,8 +15,8 @@ type ContentState = export interface DocumentInfoPanelProps { document: DocumentSummarySectionProps['document']; - summaryProps?: Omit; - metadataItems?: Array<{ label: string; value?: string }>; + summaryProps?: Omit; + metadataItems?: DocumentSummaryRow[]; metadataPayload?: Record; metadataTabLabel?: string; detailsTabLabel?: string; @@ -76,7 +76,7 @@ const DocumentInfoPanel: React.FC = ({ if (Array.isArray(metadataItemsProp) && metadataItemsProp.length) { return metadataItemsProp; } - return buildDocumentMetadataItems(document); + return describeDocumentSummary(document); }, [metadataItemsProp, document]); const metadataPayload = useMemo(() => { @@ -149,18 +149,17 @@ const DocumentInfoPanel: React.FC = ({ const renderSummarySection = useCallback(() => ( - ), [document, summaryLayout, summaryProps, metadataItems]); + ), [document, summaryLayout, summaryProps]); const renderDetailsSection = useCallback(() => (
{metadataItems.length ? (
- {metadataItems.map(({ label, value }) => ( -
+ {metadataItems.map(({ key, label, value }) => ( +
{label}
{value || '—'}
diff --git a/frontend/src/documents/DocumentSummarySection.tsx b/frontend/src/documents/DocumentSummarySection.tsx index ef7509f..46b3b5f 100644 --- a/frontend/src/documents/DocumentSummarySection.tsx +++ b/frontend/src/documents/DocumentSummarySection.tsx @@ -1,14 +1,16 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import type { FormEvent } from 'react'; -import { EditIcon, IconX, PlusIcon } from '../ui/icons'; -import SelectionAssignmentMenu, { SelectionAssignmentMenuItem } from './SelectionAssignmentMenu'; +import React, { useCallback, useEffect, useMemo, useState, type ReactNode, type FormEvent } from 'react'; +import { EditIcon, IconX, CheckIcon, PlusIcon } from '../ui/icons'; +import SelectionAssignmentMenu, { + SelectionAssignmentMenuItem, + type NormalizedSelectionAssignmentItem, +} from './SelectionAssignmentMenu'; import { getTagColorStyle } from '../utils/colors'; import { formatDate, toDateInputValue, toIssuedTimestamp, } from '../utils/date'; -import { describeDocumentSummary } from './documentSummary'; +import { describeDocumentSummary, type DocumentSummaryRow } from './documentSummary'; import { isPlainObject } from '../utils/typeGuards'; type Identifier = string | number; @@ -70,7 +72,14 @@ export interface DocumentSummarySectionProps { onUpdateTitle?: (docId: Identifier | undefined, title: string) => Promise | boolean; onUpdateIssued?: (docId: Identifier | undefined, timestamp: number | null) => Promise | boolean; layout?: 'default' | 'compact'; - detailItems?: Array<{ label?: string; value?: string }>; +} + +interface MetaItem { + key: string; + label: string; + valueContent?: React.ReactNode | null; + fallbackValue?: string | null; + error?: string | null; } export const sortCorrespondents = (entries = []) => @@ -122,7 +131,7 @@ const resolveOptionName = (source?: QuickAddOption | string | null): string => { return `${source}`.trim(); }; -const normalizeQuickAddOption = (option: QuickAddOption | string | null | undefined): QuickAddEntry | null => { +const normalizeQuickAddOption = (option?: QuickAddOption | string | null): QuickAddEntry | null => { if (option == null) { return null; } @@ -227,7 +236,7 @@ export const TagSection: React.FC = ({ }, [normalizedOptions, tags]); const handleAssignmentSelect = useCallback( - (item: SelectionAssignmentMenuItem | null) => { + (item: NormalizedSelectionAssignmentItem) => { if (!item) { return; } @@ -271,11 +280,7 @@ export const TagSection: React.FC = ({ showCounts={false} positionStrategy="fixed" triggerClassName="quick-add__chip quick-add__trigger" - triggerContent={( - - - )} + triggerContent={
@@ -435,20 +436,9 @@ const DocumentSummarySection: React.FC = ({ onUpdateTitle, onUpdateIssued, layout = 'default', - detailItems = [], }) => { const isCompactLayout = layout === 'compact'; - const summary = useMemo(() => { - if (!document) { - return { - title: '', - originalName: '', - sizeLabel: '—', - pageCount: null, - }; - } - return describeDocumentSummary(document); - }, [document]); + const summaryRows = useMemo(() => describeDocumentSummary(document), [document]); const issuedDateLabel = useMemo( () => formatDate(document?.issued_at, { fallback: null }), [document?.issued_at], @@ -475,8 +465,8 @@ const DocumentSummarySection: React.FC = ({ return sortCorrespondents(document?.correspondents || []); }, [correspondents, document?.correspondents]); - const metaRows = useMemo(() => { - const rows: { key: string; label: string; value: string | null }[] = []; + const extraSummaryRows = useMemo(() => { + const rows: DocumentSummaryRow[] = []; const currentVersionNumber = document?.current_version?.version_number; if (Number.isFinite(currentVersionNumber)) { rows.push({ @@ -485,17 +475,8 @@ const DocumentSummarySection: React.FC = ({ value: `#${currentVersionNumber}`, }); } - - if (summary.sizeLabel && summary.sizeLabel !== '—') { - rows.push({ key: 'size', label: 'Size', value: summary.sizeLabel }); - } - - if (Number.isFinite(summary.pageCount)) { - rows.push({ key: 'pages', label: 'Pages', value: String(summary.pageCount) }); - } - return rows; - }, [document?.current_version?.version_number, summary]); + }, [document?.current_version?.version_number]); const [titleDraft, setTitleDraft] = useState(''); const [titleSaving, setTitleSaving] = useState(false); @@ -595,57 +576,60 @@ const DocumentSummarySection: React.FC = ({ return null; } - const TitleSection = () => ( - editableTitle && isTitleEditing ? ( -
- { - setTitleDraft(event.target.value); - if (titleError) { - setTitleError(null); - } - }} - onKeyDown={(event) => { - if (event.key === 'Escape') { - event.preventDefault(); - cancelTitleEdit(); - } - }} - aria-label="Document title" - autoFocus - disabled={titleSaving} - /> - - -
- ) : ( - <> -

{summary.title}

- {editableTitle ? ( - - ) : null} - - ) + const renderTitleEditForm = (extraClassName?: string) => ( +
+ { + setTitleDraft(event.target.value); + if (titleError) { + setTitleError(null); + } + }} + onKeyDown={(event) => { + if (event.key === 'Escape') { + event.preventDefault(); + cancelTitleEdit(); + } + }} + aria-label="Document title" + autoFocus + disabled={titleSaving} + /> + + +
); + const titleMetaDisplay = editableTitle && isTitleEditing + ? renderTitleEditForm('doc-title-edit--inline') + : ( + <> + {document?.title} + {editableTitle ? ( + + ) : null} + + ); + const issuedDisplay = editableIssued && isIssuedEditing ? (
= ({ aria-label="Issued on" disabled={issuedSaving} /> -
) : ( @@ -689,184 +674,98 @@ const DocumentSummarySection: React.FC = ({ ); - const metaItems = [ - { - key: 'issued', - label: 'Issued', - valueContent: issuedDisplay, - error: issuedError, - }, - ...metaRows.map((row) => ({ - key: row.key, - label: row.label, - fallbackValue: row.value, - })), - ]; - - const detailRows = Array.isArray(detailItems) - ? detailItems.map((item, index) => ({ - key: `detail-${item?.label || index}`, - label: item?.label || '—', - fallbackValue: item?.value, - })) - : []; - - const compactRows = [...metaItems, ...detailRows]; - - const renderTags = () => ( -
- onTagRemove(document.id, tag.id) - : undefined - } - onAdd={ - onTagAdd - ? ({ value, option }) => onTagAdd(document, value, { option }) - : undefined - } - datalistOptions={tagOptions} - className="document-summary__tags" - /> -
+ const tagsValueContent = ( + onTagRemove(document.id, tag.id) + : undefined + } + onAdd={ + onTagAdd + ? ({ value, option }) => onTagAdd(document, value, { option }) + : undefined + } + datalistOptions={tagOptions} + className="document-summary__tags" + /> ); - const renderCorrespondents = () => ( -
- - onCorrespondentRemove({ - documentId: document.id, - correspondentId: entry.id, - }) - : undefined - } - onAdd={ - onCorrespondentAdd - ? ({ name, option }) => - onCorrespondentAdd({ - document, - name, - option, - }) - : undefined - } - showCount - datalistOptions={correspondentOptions} - className="document-summary__correspondents" - /> -
+ const correspondentsValueContent = ( + + onCorrespondentRemove({ + documentId: document.id, + correspondentId: entry.id, + }) + : undefined + } + onAdd={ + onCorrespondentAdd + ? ({ name, option }) => + onCorrespondentAdd({ + document, + name, + option, + }) + : undefined + } + showCount + datalistOptions={correspondentOptions} + className="document-summary__correspondents" + /> ); - if (isCompactLayout) { - return ( -
-
- -
- {titleError ?
{titleError}
: null} - {renderTags()} - {renderCorrespondents()} - {compactRows.length ? ( -
-
- {compactRows.map((item) => ( -
-
{item.label}
-
- {item.valueContent != null && item.valueContent !== '' - ? item.valueContent - : item.fallbackValue || '—'} -
- {item.error ?
{item.error}
: null} -
- ))} -
-
- ) : null} -
- ); - } + const summaryRowOverrides = useMemo( + () => ({ + title: { valueContent: titleMetaDisplay, error: titleError }, + issued: { valueContent: issuedDisplay, error: issuedError }, + tags: { valueContent: tagsValueContent }, + correspondents: { valueContent: correspondentsValueContent }, + }), + [titleMetaDisplay, titleError, issuedDisplay, issuedError, tagsValueContent, correspondentsValueContent], + ); + + const baseRows: MetaItem[] = useMemo( + () => [...summaryRows, ...extraSummaryRows].map((row) => { + const overrides = summaryRowOverrides[row.key] || {}; + return { + key: row.key, + label: row.label, + valueContent: overrides.valueContent ?? null, + fallbackValue: overrides.valueContent ? row.value : row.value, + error: overrides.error ?? null, + }; + }), + [summaryRows, extraSummaryRows, summaryRowOverrides], + ); + + const allRows = baseRows; + const summaryClass = `document-summary${isCompactLayout ? ' document-summary--compact' : ''}`; + const sectionClass = `document-summary__section document-summary__meta${isCompactLayout ? ' document-summary__meta--compact' : ''}`; + const listClass = `document-summary__details-list${isCompactLayout ? ' document-summary__details-list--meta' : ''}`; return ( -
-
-
- {editableTitle && isTitleEditing ? ( -
- { - setTitleDraft(event.target.value); - if (titleError) { - setTitleError(null); - } - }} - onKeyDown={(event) => { - if (event.key === 'Escape') { - event.preventDefault(); - cancelTitleEdit(); - } - }} - aria-label="Document title" - autoFocus - disabled={titleSaving} - /> - - -
- ) : ( - <> -

{summary.title}

- {editableTitle ? ( - - ) : null} - - )} -
-
- - {titleError ?
{titleError}
: null} - -
-
- Issued: - {issuedDisplay} -
- {issuedError ?
{issuedError}
: null} - - {metaRows.map((row) => ( -
- {row.label}: - {row.value} -
- ))} -
- - {renderTags()} - {renderCorrespondents()} +
+
+
+ {allRows.map((item) => ( +
+
{item.label}
+
+ {item.valueContent != null && item.valueContent !== '' + ? item.valueContent + : item.fallbackValue || '—'} +
+ {item.error ?
{item.error}
: null} +
+ ))} +
+
); }; diff --git a/frontend/src/documents/DocumentsGrid.tsx b/frontend/src/documents/DocumentsGrid.tsx index 4def7a1..f9f0c99 100644 --- a/frontend/src/documents/DocumentsGrid.tsx +++ b/frontend/src/documents/DocumentsGrid.tsx @@ -77,10 +77,10 @@ interface DocumentsGridProps { getDocumentAsset?: (...args: any[]) => unknown; gridIconSize?: number; tagLookupById?: Map | null; - onTagClick?: (tagId?: Identifier | null) => void; + onTagClick?: (tagId: Identifier) => void; scrollRef?: RefObject; - onCorrespondentClick?: (correspondentId?: Identifier | null) => void; - activeCorrespondentIdSet?: Set | null; + onCorrespondentClick?: (correspondentId: Identifier) => void; + activeCorrespondentIdSet?: Set | null; onDocumentRename?: (docId: Identifier, title: string) => Promise | boolean; } @@ -434,20 +434,26 @@ const DocumentsGrid: React.FC = ({
{visibleTags.length > 0 && (
- {visibleTags.map((tag) => { + {visibleTags.map((tag, index) => { const colorSource = tag?.color || tagLookupById?.get(tag.id)?.color; const style = getTagColorStyle(colorSource); + const tagId = tag?.id ?? null; + const clickable = tagId != null && typeof onTagClick === 'function'; + const key = tagId ?? `${doc.id}-tag-${index}`; return ( { + role={clickable ? 'button' : undefined} + onClick={clickable ? (event) => { event.stopPropagation(); - onTagClick?.(tag.id); - }} + if (tagId == null) { + return; + } + onTagClick?.(tagId); + } : undefined} draggable onDragStart={(event) => { event.stopPropagation(); @@ -463,13 +469,16 @@ const DocumentsGrid: React.FC = ({ onDragEnd={(event) => { event.stopPropagation(); }} - onKeyDown={(event) => { + onKeyDown={clickable ? (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); event.stopPropagation(); - onTagClick?.(tag.id); + if (tagId == null) { + return; + } + onTagClick?.(tagId); } - }} + } : undefined} > {tag.label} diff --git a/frontend/src/documents/DocumentsList.tsx b/frontend/src/documents/DocumentsList.tsx index 9898c29..62cc7ec 100644 --- a/frontend/src/documents/DocumentsList.tsx +++ b/frontend/src/documents/DocumentsList.tsx @@ -82,9 +82,9 @@ export interface DocumentsListProps { onDocumentTagDrop?: (event: DragEvent, documentId: Identifier) => void; onDocumentRename?: (documentId: Identifier, nextTitle: string) => Promise | boolean; tagLookupById?: Map | null; - onTagClick?: (tagId?: Identifier | null) => void; - onCorrespondentClick?: (correspondentId?: Identifier | null) => void; - activeCorrespondentIdSet?: Set | null; + onTagClick?: (tagId: Identifier) => void; + onCorrespondentClick?: (correspondentId: Identifier) => void; + activeCorrespondentIdSet?: Set | null; scrollRef?: RefObject; } @@ -456,20 +456,24 @@ const DocumentsList: React.FC = ({
{(doc.tags || []).length > 0 && (
- {(doc.tags || []).map((tag) => { + {(doc.tags || []).map((tag, index) => { const colorSource = tag?.color || tagLookupById?.get(tag.id)?.color; const style = getTagColorStyle(colorSource); + const tagId = tag?.id ?? null; + const clickable = tagId != null && typeof onTagClick === 'function'; + const key = tagId ?? `${doc.id}-tag-${index}`; return ( { + role={clickable ? 'button' : undefined} + onClick={clickable ? (event) => { event.stopPropagation(); - onTagClick?.(tag.id); - }} + if (tagId == null) return; + onTagClick?.(tagId); + } : undefined} draggable onDragStart={(event) => { event.stopPropagation(); @@ -485,13 +489,16 @@ const DocumentsList: React.FC = ({ onDragEnd={(event) => { event.stopPropagation(); }} - onKeyDown={(event) => { + onKeyDown={clickable ? (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); event.stopPropagation(); - onTagClick?.(tag.id); + if (tagId == null) { + return; + } + onTagClick?.(tagId); } - }} + } : undefined} > {tag.label} diff --git a/frontend/src/documents/SelectionAssignmentMenu.tsx b/frontend/src/documents/SelectionAssignmentMenu.tsx index 945b2db..6889694 100644 --- a/frontend/src/documents/SelectionAssignmentMenu.tsx +++ b/frontend/src/documents/SelectionAssignmentMenu.tsx @@ -29,7 +29,7 @@ export interface SelectionAssignmentMenuProps { items?: SelectionAssignmentMenuItem[]; placeholder?: string; emptyMessage?: string; - createLabel?: string | null; + createLabel?: string; onToggle?: (item: NormalizedSelectionAssignmentItem) => Promise | void; onCreate?: (value: string) => Promise | void; disabled?: boolean; @@ -82,7 +82,7 @@ const SelectionAssignmentMenu: React.FC = ({ items = [], placeholder = 'Search…', emptyMessage = 'No entries', - createLabel = null, + createLabel = 'Add', onToggle, onCreate, disabled = false, @@ -91,8 +91,8 @@ const SelectionAssignmentMenu: React.FC = ({ triggerClassName = 'quick-add__chip quick-add__trigger panel-floating-actions__trigger', showStateIndicators = true, showCounts = true, - onOpenMenu = null, - renderItemLabel = null, + onOpenMenu, + renderItemLabel, positionStrategy = 'absolute', }) => { const anchorRef = useRef(null); @@ -162,7 +162,7 @@ const SelectionAssignmentMenu: React.FC = ({ const handleToggle = useCallback( async (item: NormalizedSelectionAssignmentItem) => { - if (!item || !onToggle) { + if (!onToggle) { return; } setPending(true); @@ -265,8 +265,8 @@ const SelectionAssignmentMenu: React.FC = ({ type="submit" className="icon-button selection-assignment__add" disabled={!canSubmitCreate} - aria-label={createLabel || 'Add'} - title={createLabel || 'Add'} + aria-label={createLabel} + title={createLabel} >
{themeMenuSection} + {communityMenuFooter} , document.body, ) @@ -981,38 +1030,6 @@ const Sidebar: React.FC = ({ - ); }; diff --git a/frontend/src/styles/base/controls.css b/frontend/src/styles/base/controls.css index 68b8bbe..6f17852 100644 --- a/frontend/src/styles/base/controls.css +++ b/frontend/src/styles/base/controls.css @@ -77,6 +77,15 @@ button.danger:hover:not([disabled]) { color: var(--fg); } +.icon-button--accent { + color: var(--on-accent); + background: var(--accent); +} + +.icon-button--accent:hover:not([disabled]) { + background: var(--accent-hover); +} + .icon-button.danger { color: var(--danger); } @@ -96,4 +105,3 @@ button.danger:hover:not([disabled]) { color: var(--danger); background: transparent; } - diff --git a/frontend/src/styles/base/theme.css b/frontend/src/styles/base/theme.css index 9e0a466..b15634e 100644 --- a/frontend/src/styles/base/theme.css +++ b/frontend/src/styles/base/theme.css @@ -104,7 +104,7 @@ --surface-ink-soft: color-mix(in oklch, var(--fg) 8%, transparent); font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - font-size: clamp(13px, 0.5vw + 12px, 15px); + font-size: 100%; --font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; --detail-panel-width: calc(100vw / 3); diff --git a/frontend/src/styles/detail/detail-panels.css b/frontend/src/styles/detail/detail-panels.css index b454de8..f4dda49 100644 --- a/frontend/src/styles/detail/detail-panels.css +++ b/frontend/src/styles/detail/detail-panels.css @@ -603,7 +603,12 @@ .tag-list.document-summary__tags, .correspondent-list.document-summary__correspondents { - margin-top: 0.25rem; + display: inline-flex; + align-items: center; + gap: 0.35rem; + margin: 0; + flex-wrap: wrap; + justify-content: flex-end; } .document-summary__details { @@ -614,26 +619,40 @@ padding: 0; display: flex; flex-direction: column; - gap: 1.25rem; + font-size: 0.85rem; } .document-summary__details-row { display: flex; - flex-direction: column; - gap: 0.15rem; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 0.5rem; + border-top: 1px solid var(--border); + padding: 0.75rem 0; } .document-summary__details-row dt { margin: 0; - font-size: 0.8rem; color: var(--muted); - word-break: break-word; + white-space: nowrap; + flex: 0 0 auto; } .document-summary__details-row dd { margin: 0; - font-size: 0.95rem; word-break: break-word; + text-align: right; + flex: 1 1 auto; + display: inline-flex; + justify-content: flex-end; + align-items: center; + gap: 0.35rem; + flex-wrap: wrap; +} + +.document-summary__details-row .icon-button { + margin: -0.25rem 0; } .detail-panel .doc-title-edit, @@ -644,10 +663,40 @@ width: 100%; } +.document-summary__details-row .doc-title-edit, +.document-summary__details-row .doc-title-edit--inline { + width: auto; + flex: 1 1 auto; +} + +.doc-title-edit--inline { + display: inline-flex; + align-items: center; + gap: 0.35rem; + width: auto; +} + +.doc-title-edit--inline input { + width: auto; + min-width: 8rem; +} + .detail-panel .doc-title-edit input, .document-summary .doc-title-edit input { flex: 1; min-width: 0; + padding: 0.3rem 0.55rem; + border-radius: 4px; + border: 1px solid var(--border); + background: var(--surface); + color: var(--fg); + font: inherit; +} + +.detail-panel .doc-title-edit input:focus-visible, +.document-summary .doc-title-edit input:focus-visible { + outline: 2px solid var(--selection-ring); + outline-offset: 1px; } .status-inline { @@ -918,14 +967,7 @@ margin: 0; } -.detail-panel dt { - font-weight: 600; - margin-top: 0; -} - -.detail-panel dd { - margin: 0.2rem 0 0; -} +. .detail-panel .tag-list, .document-summary .tag-list, diff --git a/frontend/src/styles/documents/viewer.css b/frontend/src/styles/documents/viewer.css index 49e0ec5..be9820a 100644 --- a/frontend/src/styles/documents/viewer.css +++ b/frontend/src/styles/documents/viewer.css @@ -249,9 +249,7 @@ } .detail-panel .document-viewer__section-item dd { - margin: 0; font-size: 0.9rem; - font-weight: 500; } .document-viewer__section-placeholder { diff --git a/frontend/src/styles/sidebar/sidebar.css b/frontend/src/styles/sidebar/sidebar.css index 63fdef6..8cf5ddd 100644 --- a/frontend/src/styles/sidebar/sidebar.css +++ b/frontend/src/styles/sidebar/sidebar.css @@ -188,48 +188,6 @@ min-height: 0; } -.sidebar__footer { - border-top: 1px solid var(--border-muted, var(--border)); - background: var(--bg); - padding: 0.6rem 0.85rem; - display: flex; - gap: 0.35rem; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - color: var(--muted); -} - -.sidebar__footer-link { - display: inline-flex; - flex: 1 1 0; - align-items: center; - justify-content: center; - gap: 0.25rem; - font-size: 0.83rem; - color: inherit; - text-decoration: none; - padding: 0.2rem 0.35rem; - border-radius: 999px; - transition: - color 0.12s ease, - background-color 0.12s ease; -} - -.sidebar__footer-link:visited { - color: inherit; -} - -.sidebar__footer-link:hover, -.sidebar__footer-link:focus-visible { - color: var(--sidebar-fg, var(--fg)); - background-color: color-mix(in srgb, var(--surface-overlay) 20%, transparent); -} - -.sidebar__footer-link .icon { - flex-shrink: 0; -} - .sidebar__title { margin: 0; font-size: 1rem; @@ -497,6 +455,40 @@ transition: background 0.15s ease, color 0.15s ease; } +.menu__footer { + border-top: 1px solid var(--border); + margin-top: 0.35rem; + padding: 0.35rem 0.5rem; + display: flex; + gap: 0.35rem; + align-items: center; + justify-content: space-between; + flex-wrap: nowrap; + color: var(--muted); +} + +.menu__footer-link { + display: inline-flex; + align-items: center; + gap: 0.25rem; + text-decoration: none; + color: var(--muted); + font-size: 0.82rem; + padding: 0.25rem 0.4rem; + border-radius: 999px; + transition: background 0.15s ease, color 0.15s ease; +} + +.menu__footer-link:visited { + color: var(--muted); +} + +.menu__footer-link:hover, +.menu__footer-link:focus-visible { + background: var(--sidebar-hover-bg); + color: var(--fg); +} + .menu__button .menu__check-slot { width: 1rem; display: inline-flex; @@ -556,14 +548,6 @@ background: var(--sidebar-hover-bg); } -.sidebar__footer .sidebar-section { - margin-top: 0; -} - -.sidebar__footer .sidebar-section__actions { - gap: 0.4rem; -} - .sidebar-section:first-of-type, .sidebar-section--folders { margin-top: 0; diff --git a/frontend/src/tag_manager.ts b/frontend/src/tag_manager.ts index 1f207fc..93406e5 100644 --- a/frontend/src/tag_manager.ts +++ b/frontend/src/tag_manager.ts @@ -18,7 +18,7 @@ class TagManager { this.colorGenerator = colorGenerator; } - normalizeLabel(label: string | null | undefined): string { + normalizeLabel(label?: string | null): string { return label?.trim?.() || ''; } diff --git a/frontend/src/ui/QuickAddMenu.tsx b/frontend/src/ui/QuickAddMenu.tsx index 21efba5..77e4026 100644 --- a/frontend/src/ui/QuickAddMenu.tsx +++ b/frontend/src/ui/QuickAddMenu.tsx @@ -13,7 +13,7 @@ interface NormalizedOption { index: number; } -const normalizeOption = (option: QuickAddOption | null | undefined, index: number): NormalizedOption | null => { +const normalizeOption = (option: QuickAddOption | null, index: number): NormalizedOption | null => { if (option == null) { return null; } diff --git a/frontend/src/utils/colors.ts b/frontend/src/utils/colors.ts index 64fd7f9..b9f092f 100644 --- a/frontend/src/utils/colors.ts +++ b/frontend/src/utils/colors.ts @@ -85,7 +85,7 @@ const rgbToHsl = ({ r, g, b }: RgbColor) => { return { h: hue, s: clamp01(saturation), l: clamp01(lightness) }; }; -export const hexToRgb = (input: string | null | undefined): (RgbColor & { hex: string }) | null => { +export const hexToRgb = (input?: string): (RgbColor & { hex: string }) | null => { if (!input) return null; const match = HEX_COLOR_PATTERN.exec(input.trim()); if (!match) return null; diff --git a/frontend/src/utils/date.ts b/frontend/src/utils/date.ts index cc22948..befd37f 100644 --- a/frontend/src/utils/date.ts +++ b/frontend/src/utils/date.ts @@ -1,4 +1,4 @@ -const ensureDate = (value: string | number | Date | null | undefined): Date | null => { +const ensureDate = (value: string | number | Date | null): Date | null => { if (!value) { return null; } @@ -12,7 +12,7 @@ interface FormatOptions { options?: Intl.DateTimeFormatOptions; } -export const formatDate = (value: string | number | Date | null | undefined, { fallback = '—', locale, options }: FormatOptions = {}): string => { +export const formatDate = (value: string | number | Date | null, { fallback = '—', locale, options }: FormatOptions = {}): string => { const date = ensureDate(value); if (!date) { return fallback; @@ -20,7 +20,7 @@ export const formatDate = (value: string | number | Date | null | undefined, { f return date.toLocaleDateString(locale, options); }; -export const formatDateTime = (value: string | number | Date | null | undefined, { fallback = '—', locale, options }: FormatOptions = {}): string => { +export const formatDateTime = (value: string | number | Date | null, { fallback = '—', locale, options }: FormatOptions = {}): string => { const date = ensureDate(value); if (!date) { return fallback; @@ -28,7 +28,7 @@ export const formatDateTime = (value: string | number | Date | null | undefined, return date.toLocaleString(locale, options); }; -export const toDateInputValue = (value: string | number | Date | null | undefined): string => { +export const toDateInputValue = (value: string | number | Date | null): string => { const date = ensureDate(value); if (!date) { return ''; @@ -38,7 +38,7 @@ export const toDateInputValue = (value: string | number | Date | null | undefine return localDate.toISOString().slice(0, 10); }; -export const toIssuedTimestamp = (dateString: string | null | undefined, fallback: string | number | Date | null | undefined): string | null => { +export const toIssuedTimestamp = (dateString: string | null, fallback: string | number | Date | null): string | null => { if (!dateString) { return null; } @@ -52,7 +52,7 @@ export const toIssuedTimestamp = (dateString: string | null | undefined, fallbac return Number.isNaN(candidate.getTime()) ? null : candidate.toISOString(); }; -export const parseDateValue = (value: string | number | Date | null | undefined): Date | null => ensureDate(value); +export const parseDateValue = (value: string | number | Date | null): Date | null => ensureDate(value); export default { formatDate, diff --git a/frontend/src/utils/ocr.ts b/frontend/src/utils/ocr.ts index 23715b1..95ef5cb 100644 --- a/frontend/src/utils/ocr.ts +++ b/frontend/src/utils/ocr.ts @@ -18,12 +18,12 @@ export interface DocumentLike extends AssetManagerDocumentLike { export type AssetLike = AssetManagerAssetLike; -export type EnsurePreviewData = (id: string | number) => Promise; +export type EnsurePreviewData = (id: string | number) => Promise; export type EnsureAssetUrl = ( id: string | number, asset: AssetLike, options?: { start?: number; limit?: number; force?: boolean }, -) => Promise; +) => Promise; export type GetDocumentAsset = AssetManagerGetAsset; interface ResolveOcrTextUrlOptions { @@ -33,7 +33,7 @@ interface ResolveOcrTextUrlOptions { ensureAssetUrl?: EnsureAssetUrl; } -const pickAsset = (doc: DocumentLike | null | undefined, getDocumentAsset?: GetDocumentAsset): AssetLike | null => { +const pickAsset = (doc?: DocumentLike | null, getDocumentAsset?: GetDocumentAsset): AssetLike | null => { if (!doc || !getDocumentAsset) { return null; }