diff --git a/frontend/src/asset_manager.ts b/frontend/src/asset_manager.ts index 873afda..5742bb3 100644 --- a/frontend/src/asset_manager.ts +++ b/frontend/src/asset_manager.ts @@ -1,10 +1,10 @@ import type { Identifier } from './types/identifiers'; -import type { AssetObject, AssetLike } from './types/assets'; +import type { Asset } from './types/assets'; import type { DocumentVersion, Document } from './types/documents'; type Nullable = T | null; -export type { AssetObject, AssetLike, DocumentVersion as DocumentVersionLike, Document }; +export type { Asset, DocumentVersion as DocumentVersionLike, Document }; export const resolveAssetExpiresAt = (asset?: { download?: { expires_at: number } | null } | null): number | null => asset?.download?.expires_at ?? null; @@ -14,16 +14,16 @@ export const resolveAssetUrl = (asset?: { download?: { url: string } | null } | export type EnsureAssetUrl = ( documentId: Identifier, - asset: AssetLike, + asset: Asset, options?: { force?: boolean;[key: string]: unknown }, ) => Promise; -export type GetAsset = (document: Document, assetType: string) => Nullable; +export type GetAsset = (document: Document, assetType: string) => Nullable; export const getAssetFromGroup = ( - assets?: AssetLike[] | Record | null, + assets?: Asset[] | Record | null, assetType: string = '', -): Nullable => { +): Nullable => { if (!assetType || !assets) { return null; } @@ -42,97 +42,7 @@ export const getAssetFromVersion = (currentVersion: Nullable, a return getAssetFromGroup(currentVersion.assets, assetType); }; -const normalizeAssetObjects = (objects?: AssetObject[] | null): AssetObject[] => { - if (!Array.isArray(objects)) { - return []; - } - return objects - .filter((entry) => Number.isInteger(entry?.ordinal)) - .slice() - .sort((a, b) => a.ordinal - b.ordinal); -}; -export class AssetView { - asset: AssetLike | null; - private _objectsRef: AssetObject[] | null; - private _sortedObjects: AssetObject[]; - - constructor(asset?: AssetLike | null) { - this.asset = asset || null; - this._objectsRef = null; - this._sortedObjects = []; - } - - getCardinality(): number { - if (!this.asset) { - return 0; - } - - const objectsCount = this.getObjects().length; - if (objectsCount > 0) { - return objectsCount; - } - - if (this.asset.url || this.asset.metadata) { - return 1; - } - - return 0; - } - - getObjects(): AssetObject[] { - if (!this.asset || !Array.isArray(this.asset.objects) || this.asset.objects.length === 0) { - return []; - } - - if (this._objectsRef === this.asset.objects) { - return this._sortedObjects; - } - - this._objectsRef = this.asset.objects; - this._sortedObjects = normalizeAssetObjects(this.asset.objects); - return this._sortedObjects; - } - - getObject(ordinal = 1): AssetObject | null { - const fromObjects = this.getObjects().find((entry) => entry.ordinal === ordinal); - if (fromObjects) { - return fromObjects; - } - - if (ordinal === 1 && this.asset) { - const primaryUrl = resolveAssetUrl(this.asset); - if (primaryUrl || this.asset.metadata) { - return { - ordinal: 1, - url: primaryUrl || null, - metadata: this.asset.metadata || null, - expires_at: resolveAssetExpiresAt(this.asset), - }; - } - } - - return null; - } - - getPrimaryObject(): AssetObject | null { - return this.getObject(1); - } - - getPrimaryMetadata(): Record | null { - return this.getPrimaryObject()?.metadata || null; - } - - getPrimaryUrl(): string | null { - return this.getPrimaryObject()?.url || null; - } - - hasObject(ordinal: number): boolean { - return Boolean(this.getObject(ordinal)); - } -} - -export const createAssetView = (asset?: AssetLike | null): AssetView => new AssetView(asset); export const resolveDocumentAssetUrl = ( doc: Nullable, @@ -154,10 +64,8 @@ export const resolveDocumentAssetUrl = ( if (!asset) { return null; } - const view = createAssetView(asset); - const object = view.getPrimaryObject(); - const url = object?.url || view.getPrimaryUrl(); - const expiresAt = object?.expires_at ?? resolveAssetExpiresAt(asset); + const url = resolveAssetUrl(asset); + const expiresAt = resolveAssetExpiresAt(asset); const now = Date.now(); if (url && (!expiresAt || expiresAt > now)) { return url; @@ -174,22 +82,22 @@ export const resolveDocumentAssetUrl = ( }; class AssetManager { - fetchAsset: ((id: Identifier) => Promise) | null; + fetchAsset: ((id: Identifier) => Promise) | null; - assetCache: Map; - assetInflight: Map>; + assetCache: Map; + assetInflight: Map>; - constructor({ fetchAsset }: { fetchAsset: ((id: Identifier) => Promise) | null }) { + constructor({ fetchAsset }: { fetchAsset: ((id: Identifier) => Promise) | null }) { this.fetchAsset = fetchAsset; this.assetCache = new Map(); this.assetInflight = new Map(); } - setFetchAsset(fetchAsset: ((id: Identifier) => Promise) | null) { + setFetchAsset(fetchAsset: ((id: Identifier) => Promise) | null) { this.fetchAsset = fetchAsset; } - rememberAsset(entry?: Nullable) { + rememberAsset(entry?: Nullable) { if (entry?.id) { this.assetCache.set(entry.id, entry); } @@ -197,26 +105,18 @@ class AssetManager { ensureAsset( documentId?: Identifier | null, - asset?: Nullable, + asset?: Nullable, { force = false }: { force?: boolean } = {}, - ): Promise> { + ): Promise> { if (!documentId || !asset?.id) { return Promise.resolve(asset); } const baseAsset = this.assetCache.get(asset.id) || asset; - const view = createAssetView(baseAsset); const assetExpiresAt = resolveAssetExpiresAt(baseAsset); const now = Date.now(); const isPrimarySatisfied = () => { - const object = view.getObject(1); - if (object?.url) { - const objectExpiresAt = object.expires_at ?? null; - if (!objectExpiresAt || objectExpiresAt > now) { - return true; - } - } const assetUrl = resolveAssetUrl(baseAsset); if (assetUrl && (!assetExpiresAt || assetExpiresAt > now)) { return true; @@ -243,7 +143,7 @@ class AssetManager { return Promise.reject(new Error('AssetManager fetcher is not configured.')); } - const request: Promise = this.fetchAsset(asset.id) + const request: Promise = this.fetchAsset(asset.id) .then((data) => { if (!data) return null; const cachedEntry = this.assetCache.get(asset.id) || baseAsset; diff --git a/frontend/src/desktop/DesktopPreviewCard.tsx b/frontend/src/desktop/DesktopPreviewCard.tsx index 3925edb..27be170 100644 --- a/frontend/src/desktop/DesktopPreviewCard.tsx +++ b/frontend/src/desktop/DesktopPreviewCard.tsx @@ -1,24 +1,18 @@ -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import type { JSX } from 'react'; -import { useAssetNavigator } from '../hooks/useAssetNavigator'; +import { resolveDocumentAssetUrl } from '../asset_manager'; import type { Identifier } from '../types/identifiers'; import type { Document } from '../types/documents'; -interface AssetLike { - id?: Identifier; - url?: string | null; - expires_at?: number | null; - metadata?: Record | null; - [key: string]: unknown; -} +import type { Asset } from '../types/assets'; type EnsureAssetUrl = ( documentId: Identifier, - asset: AssetLike, + asset: Asset, options?: { force?: boolean;[key: string]: unknown }, ) => Promise; -type GetDocumentAsset = (document: Document | null, assetType: string) => AssetLike | null; +type GetDocumentAsset = (document: Document | null, assetType: string) => Asset | null; interface NavigatorSnapshot { url: string | null; @@ -44,14 +38,19 @@ const DesktopPreviewCard = ({ onNavigatorSnapshot, shouldLoad = true, }: DesktopPreviewCardProps): JSX.Element => { - const navigator = useAssetNavigator({ - document: doc, - assetType: 'thumbnail', - ensureAssetUrl: shouldLoad ? ensureAssetUrl : null, - getAsset: getDocumentAsset, - }); + const currentUrl = useMemo(() => { + if (!doc) return null; + return resolveDocumentAssetUrl(doc, 'thumbnail', { + ensureAssetUrl: shouldLoad && ensureAssetUrl ? ensureAssetUrl : undefined, + getAsset: getDocumentAsset, + }); + }, [doc, ensureAssetUrl, getDocumentAsset, shouldLoad]); - const { currentUrl, currentMetadata } = navigator; + const currentMetadata = useMemo(() => { + if (!doc || !getDocumentAsset) return null; + const asset = getDocumentAsset(doc, 'thumbnail'); + return asset?.metadata || null; + }, [doc, getDocumentAsset]); const docId = doc?.id ?? null; const metadataWidth = Number((currentMetadata as { width?: number } | null)?.width); diff --git a/frontend/src/desktop/hooks/usePreviewMetadata.ts b/frontend/src/desktop/hooks/usePreviewMetadata.ts index 86dfa04..6cba541 100644 --- a/frontend/src/desktop/hooks/usePreviewMetadata.ts +++ b/frontend/src/desktop/hooks/usePreviewMetadata.ts @@ -2,10 +2,7 @@ import { useEffect, useState, useRef } from 'react'; import type { DocumentId } from '../../types/identifiers'; import type { Document } from '../../types/documents'; -interface AssetLike { - id?: string; - [key: string]: unknown; -} +import type { Asset } from '../../types/assets'; interface PreviewMetadataEntry { docId: DocumentId; @@ -13,8 +10,8 @@ interface PreviewMetadataEntry { height: number; } -type GetDocumentAsset = (doc: Document, type: string) => AssetLike | null; -type EnsureAssetUrl = (docId: DocumentId, asset: AssetLike, options?: { force?: boolean }) => Promise; +type GetDocumentAsset = (doc: Document, type: string) => Asset | null; +type EnsureAssetUrl = (docId: DocumentId, asset: Asset, options?: { force?: boolean }) => Promise; const usePreviewMetadata = ( documents: Document[] | null, diff --git a/frontend/src/documents/DocumentThumbnailImage.tsx b/frontend/src/documents/DocumentThumbnailImage.tsx index b172c97..b8adfcc 100644 --- a/frontend/src/documents/DocumentThumbnailImage.tsx +++ b/frontend/src/documents/DocumentThumbnailImage.tsx @@ -8,7 +8,7 @@ import { } from '../asset_manager'; import { DEFAULT_THUMBNAIL_SIZE } from '../constants/documents'; import type { - AssetLike as AssetManagerAssetLike, + Asset as AssetManagerAsset, EnsureAssetUrl as AssetManagerEnsureAssetUrl, GetAsset as AssetManagerGetAsset, } from '../asset_manager'; @@ -72,7 +72,7 @@ const getPageCount = (doc?: Document | null) => { return Number.isFinite(count) ? Number(count) : null; }; -type AssetLike = AssetManagerAssetLike; +type Asset = AssetManagerAsset; type EnsureAssetUrl = AssetManagerEnsureAssetUrl; type GetDocumentAsset = AssetManagerGetAsset; @@ -97,7 +97,7 @@ const DocumentThumbnailImage = ({ const { ref: visibilityRef, isVisible } = useLazyVisibility(scrollRootRef, documentId); const resolvedMaxSize = Math.max(1, Math.round(maxSize || 1)); - const thumbnailAsset = useMemo( + const thumbnailAsset = useMemo( () => getAssetFromVersion(document?.current_version, 'thumbnail'), [document?.current_version], ); diff --git a/frontend/src/hooks/useAssetNavigator.ts b/frontend/src/hooks/useAssetNavigator.ts deleted file mode 100644 index c938f9d..0000000 --- a/frontend/src/hooks/useAssetNavigator.ts +++ /dev/null @@ -1,113 +0,0 @@ - -import { useEffect, useMemo, useState } from 'react'; -import { resolveAssetUrl } from '../asset_manager'; -import type { Identifier } from '../types/identifiers'; -import type { Document } from '../types/documents'; - -type AssetObject = { - url?: string | null; - metadata?: Record | null; - expires_at?: number | null; - [key: string]: unknown; -}; - -type AssetLike = { - id?: Identifier; - url?: string | null; - expires_at?: number | null; - metadata?: Record | null; - objects?: AssetObject[]; - [key: string]: unknown; -}; - -type EnsureAssetUrl = ( - documentId: Identifier, - asset: AssetLike, - options?: { force?: boolean;[key: string]: unknown }, -) => Promise; - -type GetAsset = (document: Document, assetType: string) => AssetLike | null; - -type AssetViewLike = { - url: string | null; - metadata: Record | null; -}; - -interface UseAssetNavigatorOptions { - document?: Document | null; - assetType: string; - ensureAssetUrl?: EnsureAssetUrl | null; - getAsset?: GetAsset; -} - -interface AssetNavigatorReturn { - document: Document | null; - documentId: Identifier | null; - asset: AssetLike | null; - assetType: string; - currentUrl: string | null; - currentMetadata: Record | null; - isLoading: boolean; -} - -export const useAssetNavigator = ({ - document, - assetType, - ensureAssetUrl, - getAsset, -}: UseAssetNavigatorOptions): AssetNavigatorReturn => { - const documentId = (document?.id ?? null) as Identifier | null; - - const asset = useMemo(() => { - if (!document || !getAsset) { - return null; - } - return getAsset(document, assetType) || null; - }, [document, assetType, getAsset]); - - const view = useMemo( - () => ({ - url: resolveAssetUrl(asset), - metadata: (asset?.metadata as Record | null) || null, - }), - [asset], - ); - - const currentUrl = view.url || null; - const currentMetadata = view.metadata || null; - - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - if (!ensureAssetUrl || !documentId || !asset) { - return undefined; - } - if (currentUrl) { - return undefined; - } - let cancelled = false; - setIsLoading(true); - ensureAssetUrl(documentId, asset, { force: true }) - .catch(() => { }) - .finally(() => { - if (!cancelled) { - setIsLoading(false); - } - }); - return () => { - cancelled = true; - }; - }, [asset, currentUrl, documentId, ensureAssetUrl]); - - return { - document, - documentId, - asset, - assetType, - currentUrl, - currentMetadata, - isLoading, - }; -}; - -export default useAssetNavigator; diff --git a/frontend/src/preview/DocumentViewerPanel.tsx b/frontend/src/preview/DocumentViewerPanel.tsx index 21e0a22..ba76a14 100644 --- a/frontend/src/preview/DocumentViewerPanel.tsx +++ b/frontend/src/preview/DocumentViewerPanel.tsx @@ -27,14 +27,15 @@ import useViewerLayoutMode from './useViewerLayoutMode'; import { usePanelResizeBindings } from '../app/PanelManagerContext'; import type { DocumentId, FolderId } from '../types/identifiers'; import type { Document } from '../types/documents'; -import type { AssetLike } from '../types/assets'; +import type { Asset } from '../types/assets'; type SidebarMode = 'overlay' | 'inline'; interface DocumentViewerPanelProps extends DocumentSummarySectionProps { document: Document | null; - ensureAssetUrl?: (docId: DocumentId, asset: AssetLike, options?: { force?: boolean }) => Promise; - getDocumentAsset?: (doc: Document | null, type: string) => AssetLike | null; + + ensureAssetUrl?: (documentId: DocumentId, asset: Asset, options?: { force?: boolean }) => Promise; + getDocumentAsset?: (doc: Document | null, type: string) => Asset | null; ensurePreviewData?: (docId: DocumentId, options?: { signal?: AbortSignal }) => Promise; notifyApiError?: (error: unknown, fallbackMessage?: string) => void; sidebarToggle?: ReactNode; diff --git a/frontend/src/types/assets.ts b/frontend/src/types/assets.ts index 5a5ed12..9e637bc 100644 --- a/frontend/src/types/assets.ts +++ b/frontend/src/types/assets.ts @@ -1,21 +1,10 @@ import type { Identifier } from './identifiers'; import type { Download } from './common'; -export interface AssetObject { - ordinal?: number; - url?: string | null; - metadata?: Record | null; - expires_at?: number; - [key: string]: unknown; -} - -export interface AssetLike { +export interface Asset { id?: Identifier; asset_type?: string; - cardinality?: number | null; download?: Download | null; metadata?: Record | null; - assets?: Record | AssetLike[] | null; - objects?: AssetObject[] | null; [key: string]: unknown; } diff --git a/frontend/src/types/documents.ts b/frontend/src/types/documents.ts index db2aa48..a49f0bd 100644 --- a/frontend/src/types/documents.ts +++ b/frontend/src/types/documents.ts @@ -1,5 +1,5 @@ import type { Identifier } from './identifiers'; -import type { AssetLike } from './assets'; +import type { Asset } from './assets'; import type { Download } from './common'; export interface DocumentTag { @@ -15,7 +15,7 @@ export interface DocumentCorrespondent { } export interface DocumentVersion { - assets?: Record | AssetLike[] | null; + assets?: Record | Asset[] | null; metadata?: Record & { page_count?: number } | null; size_bytes?: number | string | null; checksum?: string | null; diff --git a/frontend/src/utils/ocr.ts b/frontend/src/utils/ocr.ts index 6e6c106..89aaa64 100644 --- a/frontend/src/utils/ocr.ts +++ b/frontend/src/utils/ocr.ts @@ -6,16 +6,16 @@ import type { Document, DocumentVersion, } from '../types/documents'; -import type { AssetLike } from '../types/assets'; +import type { Asset } from '../types/assets'; -export type { Document, DocumentVersion, AssetLike }; +export type { Document, DocumentVersion, Asset }; export type EnsurePreviewData = (id: string) => Promise; export type EnsureAssetUrl = ( id: string, - asset: AssetLike, + asset: Asset, options?: { force?: boolean }, -) => Promise; +) => Promise; export type GetDocumentAsset = AssetManagerGetAsset; interface ResolveTextContentUrlOptions { @@ -25,7 +25,7 @@ interface ResolveTextContentUrlOptions { ensureAssetUrl?: EnsureAssetUrl; } -const pickAsset = (doc?: Document | null, getDocumentAsset?: GetDocumentAsset): AssetLike | null => { +const pickAsset = (doc?: Document | null, getDocumentAsset?: GetDocumentAsset): Asset | null => { if (!doc || !getDocumentAsset) { return null; } @@ -60,7 +60,7 @@ export async function resolveTextContentUrl({ const baseUrl = resolveAssetUrl(asset); const hasUrl = Boolean(baseUrl); - let entry: AssetLike = asset; + let entry: Asset = asset; if (ensureAssetUrl) { const ensureOptions = { force: !hasUrl }; const ensured = await ensureAssetUrl(docRef.id!, asset, ensureOptions);