cleanup
This commit is contained in:
+17
-117
@@ -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> = 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<unknown>;
|
||||
|
||||
export type GetAsset = (document: Document, assetType: string) => Nullable<AssetLike>;
|
||||
export type GetAsset = (document: Document, assetType: string) => Nullable<Asset>;
|
||||
|
||||
export const getAssetFromGroup = (
|
||||
assets?: AssetLike[] | Record<string, AssetLike> | null,
|
||||
assets?: Asset[] | Record<string, Asset> | null,
|
||||
assetType: string = '',
|
||||
): Nullable<AssetLike> => {
|
||||
): Nullable<Asset> => {
|
||||
if (!assetType || !assets) {
|
||||
return null;
|
||||
}
|
||||
@@ -42,97 +42,7 @@ export const getAssetFromVersion = (currentVersion: Nullable<DocumentVersion>, 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<string, unknown> | 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<Document>,
|
||||
@@ -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<AssetLike | null>) | null;
|
||||
fetchAsset: ((id: Identifier) => Promise<Asset | null>) | null;
|
||||
|
||||
assetCache: Map<Identifier, AssetLike>;
|
||||
assetInflight: Map<string, Promise<AssetLike | null>>;
|
||||
assetCache: Map<Identifier, Asset>;
|
||||
assetInflight: Map<string, Promise<Asset | null>>;
|
||||
|
||||
constructor({ fetchAsset }: { fetchAsset: ((id: Identifier) => Promise<AssetLike | null>) | null }) {
|
||||
constructor({ fetchAsset }: { fetchAsset: ((id: Identifier) => Promise<Asset | null>) | null }) {
|
||||
this.fetchAsset = fetchAsset;
|
||||
this.assetCache = new Map();
|
||||
this.assetInflight = new Map();
|
||||
}
|
||||
|
||||
setFetchAsset(fetchAsset: ((id: Identifier) => Promise<AssetLike | null>) | null) {
|
||||
setFetchAsset(fetchAsset: ((id: Identifier) => Promise<Asset | null>) | null) {
|
||||
this.fetchAsset = fetchAsset;
|
||||
}
|
||||
|
||||
rememberAsset(entry?: Nullable<AssetLike>) {
|
||||
rememberAsset(entry?: Nullable<Asset>) {
|
||||
if (entry?.id) {
|
||||
this.assetCache.set(entry.id, entry);
|
||||
}
|
||||
@@ -197,26 +105,18 @@ class AssetManager {
|
||||
|
||||
ensureAsset(
|
||||
documentId?: Identifier | null,
|
||||
asset?: Nullable<AssetLike>,
|
||||
asset?: Nullable<Asset>,
|
||||
{ force = false }: { force?: boolean } = {},
|
||||
): Promise<Nullable<AssetLike>> {
|
||||
): Promise<Nullable<Asset>> {
|
||||
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<AssetLike | null> = this.fetchAsset(asset.id)
|
||||
const request: Promise<Asset | null> = this.fetchAsset(asset.id)
|
||||
.then((data) => {
|
||||
if (!data) return null;
|
||||
const cachedEntry = this.assetCache.get(asset.id) || baseAsset;
|
||||
|
||||
@@ -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<string, unknown> | 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<unknown>;
|
||||
|
||||
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);
|
||||
|
||||
@@ -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<AssetLike | null>;
|
||||
type GetDocumentAsset = (doc: Document, type: string) => Asset | null;
|
||||
type EnsureAssetUrl = (docId: DocumentId, asset: Asset, options?: { force?: boolean }) => Promise<Asset | null>;
|
||||
|
||||
const usePreviewMetadata = (
|
||||
documents: Document[] | null,
|
||||
|
||||
@@ -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<AssetLike | null>(
|
||||
const thumbnailAsset = useMemo<Asset | null>(
|
||||
() => getAssetFromVersion(document?.current_version, 'thumbnail'),
|
||||
[document?.current_version],
|
||||
);
|
||||
|
||||
@@ -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<string, unknown> | null;
|
||||
expires_at?: number | null;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
type AssetLike = {
|
||||
id?: Identifier;
|
||||
url?: string | null;
|
||||
expires_at?: number | null;
|
||||
metadata?: Record<string, unknown> | null;
|
||||
objects?: AssetObject[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
type EnsureAssetUrl = (
|
||||
documentId: Identifier,
|
||||
asset: AssetLike,
|
||||
options?: { force?: boolean;[key: string]: unknown },
|
||||
) => Promise<unknown>;
|
||||
|
||||
type GetAsset = (document: Document, assetType: string) => AssetLike | null;
|
||||
|
||||
type AssetViewLike = {
|
||||
url: string | null;
|
||||
metadata: Record<string, unknown> | 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<string, unknown> | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const useAssetNavigator = ({
|
||||
document,
|
||||
assetType,
|
||||
ensureAssetUrl,
|
||||
getAsset,
|
||||
}: UseAssetNavigatorOptions): AssetNavigatorReturn => {
|
||||
const documentId = (document?.id ?? null) as Identifier | null;
|
||||
|
||||
const asset = useMemo<AssetLike | null>(() => {
|
||||
if (!document || !getAsset) {
|
||||
return null;
|
||||
}
|
||||
return getAsset(document, assetType) || null;
|
||||
}, [document, assetType, getAsset]);
|
||||
|
||||
const view = useMemo<AssetViewLike>(
|
||||
() => ({
|
||||
url: resolveAssetUrl(asset),
|
||||
metadata: (asset?.metadata as Record<string, unknown> | 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;
|
||||
@@ -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<unknown>;
|
||||
getDocumentAsset?: (doc: Document | null, type: string) => AssetLike | null;
|
||||
|
||||
ensureAssetUrl?: (documentId: DocumentId, asset: Asset, options?: { force?: boolean }) => Promise<unknown>;
|
||||
getDocumentAsset?: (doc: Document | null, type: string) => Asset | null;
|
||||
ensurePreviewData?: (docId: DocumentId, options?: { signal?: AbortSignal }) => Promise<Document | null>;
|
||||
notifyApiError?: (error: unknown, fallbackMessage?: string) => void;
|
||||
sidebarToggle?: ReactNode;
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
import type { Identifier } from './identifiers';
|
||||
import type { Download } from './common';
|
||||
|
||||
export interface AssetObject {
|
||||
ordinal?: number;
|
||||
url?: string | null;
|
||||
metadata?: Record<string, unknown> | 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<string, unknown> | null;
|
||||
assets?: Record<string, AssetLike> | AssetLike[] | null;
|
||||
objects?: AssetObject[] | null;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -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<string, AssetLike> | AssetLike[] | null;
|
||||
assets?: Record<string, Asset> | Asset[] | null;
|
||||
metadata?: Record<string, unknown> & { page_count?: number } | null;
|
||||
size_bytes?: number | string | null;
|
||||
checksum?: string | null;
|
||||
|
||||
@@ -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<Document | null>;
|
||||
export type EnsureAssetUrl = (
|
||||
id: string,
|
||||
asset: AssetLike,
|
||||
asset: Asset,
|
||||
options?: { force?: boolean },
|
||||
) => Promise<AssetLike | null>;
|
||||
) => Promise<Asset | null>;
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user