feat: Add Download type and document download field, unify preview zoom styling
This commit is contained in:
@@ -81,7 +81,6 @@ const DocumentsRouteContent: React.FC = () => {
|
||||
return (
|
||||
<DocumentsFilterProvider value={documentsFilter}>
|
||||
<PreviewProvider
|
||||
ensureDownloadUrl={surfaceConfig.documentsTableProps?.ensureDownloadUrl}
|
||||
onNavigate={handleDocumentNavigate}
|
||||
>
|
||||
{content}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { CSSProperties, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { clamp } from '../utils/math';
|
||||
import PdfViewer from '../preview/PdfViewer';
|
||||
import MediaViewer from '../preview/MediaViewer';
|
||||
import PanelHeader from '../ui/PanelHeader';
|
||||
@@ -10,200 +9,73 @@ import type { Document } from '../types/documents';
|
||||
|
||||
interface PreviewZoomOverlayProps {
|
||||
open?: boolean;
|
||||
onClose?: () => void;
|
||||
onClose: () => void;
|
||||
onMaximize?: () => void;
|
||||
document?: Document | null;
|
||||
}
|
||||
|
||||
type NaturalSize = { width: number | null; height: number | null };
|
||||
type FocusPoint = { xRatio: number; yRatio: number } | null;
|
||||
type DisplayKind = 'image' | 'pdf';
|
||||
|
||||
type DocumentLink = {
|
||||
url: string;
|
||||
alt?: string;
|
||||
mimeType?: string | null;
|
||||
};
|
||||
|
||||
type DocumentWithPreview = Document & { documentLink?: DocumentLink };
|
||||
|
||||
const determineDisplayKind = (entry?: DocumentLink | null): DisplayKind => {
|
||||
const type = entry?.mimeType?.toLowerCase?.() || '';
|
||||
const determineDisplayKind = (doc?: Document | null): DisplayKind => {
|
||||
const type = doc?.mime_type?.toLowerCase?.() || '';
|
||||
if (type.includes('pdf')) {
|
||||
return 'pdf';
|
||||
}
|
||||
if (type.startsWith('image/')) {
|
||||
return 'image';
|
||||
}
|
||||
const url = entry?.url?.toLowerCase?.() || '';
|
||||
if (url.endsWith('.pdf')) {
|
||||
return 'pdf';
|
||||
}
|
||||
return 'image';
|
||||
};
|
||||
|
||||
const noop = () => { };
|
||||
|
||||
const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
||||
open = false,
|
||||
onClose = noop,
|
||||
onClose,
|
||||
onMaximize,
|
||||
document: overlayDocument = null,
|
||||
document: inputDocument = null,
|
||||
}) => {
|
||||
const portalTarget = document.body;
|
||||
const [isNativeScale, setIsNativeScale] = useState(false);
|
||||
const [naturalSize, setNaturalSize] = useState<NaturalSize>({ width: null, height: null });
|
||||
const [renderBackdrop, setRenderBackdrop] = useState(false);
|
||||
const [isBackdropVisible, setBackdropVisible] = useState(false);
|
||||
const [documentSnapshot, setDocumentSnapshot] = useState<DocumentWithPreview | null>(null);
|
||||
const lastDocumentRef = useRef<Document | null>(null);
|
||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||
const mediaRef = useRef<HTMLImageElement | null>(null);
|
||||
const focusRef = useRef<FocusPoint>(null);
|
||||
const previouslyFocusedRef = useRef<HTMLElement | null>(null);
|
||||
const visibilityTimerRef = useRef<number | null>(null);
|
||||
const displayTimerRef = useRef<number | null>(null);
|
||||
const timerRef = useRef<number | null>(null);
|
||||
|
||||
const currentDocument = overlayDocument;
|
||||
if (inputDocument) {
|
||||
lastDocumentRef.current = inputDocument;
|
||||
}
|
||||
|
||||
const downloadUrl = lastDocumentRef.current?.current_version?.download?.url;
|
||||
const displayKind = determineDisplayKind(lastDocumentRef.current);
|
||||
const documentTitle = lastDocumentRef.current?.title || undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (currentDocument?.documentLink?.url) {
|
||||
setDocumentSnapshot(currentDocument);
|
||||
}
|
||||
}, [currentDocument]);
|
||||
|
||||
const activeDocument = open && currentDocument?.documentLink?.url ? currentDocument : documentSnapshot;
|
||||
const documentLink = activeDocument?.documentLink || null;
|
||||
const displayKind = useMemo(() => determineDisplayKind(documentLink), [documentLink]);
|
||||
const isPdfDisplay = displayKind === 'pdf';
|
||||
const documentTitle = activeDocument?.title || undefined;
|
||||
const effectiveAlt = documentLink?.alt || documentTitle || 'Document preview';
|
||||
|
||||
useEffect(() => {
|
||||
if (visibilityTimerRef.current) {
|
||||
clearTimeout(visibilityTimerRef.current);
|
||||
visibilityTimerRef.current = null;
|
||||
}
|
||||
if (displayTimerRef.current) {
|
||||
cancelAnimationFrame(displayTimerRef.current);
|
||||
displayTimerRef.current = null;
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
cancelAnimationFrame(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
|
||||
if (open && documentLink?.url) {
|
||||
if (open) {
|
||||
setRenderBackdrop(true);
|
||||
displayTimerRef.current = requestAnimationFrame(() => {
|
||||
displayTimerRef.current = requestAnimationFrame(() => {
|
||||
timerRef.current = requestAnimationFrame(() => {
|
||||
timerRef.current = requestAnimationFrame(() => {
|
||||
setBackdropVisible(true);
|
||||
scrollRef.current?.focus?.({ preventScroll: true });
|
||||
timerRef.current = null;
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
if (displayTimerRef.current) {
|
||||
cancelAnimationFrame(displayTimerRef.current);
|
||||
displayTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
setBackdropVisible(false);
|
||||
timerRef.current = window.setTimeout(() => {
|
||||
setRenderBackdrop(false);
|
||||
timerRef.current = null;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
setBackdropVisible(false);
|
||||
visibilityTimerRef.current = window.setTimeout(() => {
|
||||
setRenderBackdrop(false);
|
||||
}, 260);
|
||||
|
||||
return () => {
|
||||
if (visibilityTimerRef.current) {
|
||||
clearTimeout(visibilityTimerRef.current);
|
||||
visibilityTimerRef.current = null;
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
cancelAnimationFrame(timerRef.current);
|
||||
}
|
||||
};
|
||||
}, [open, documentLink?.url]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (visibilityTimerRef.current) {
|
||||
clearTimeout(visibilityTimerRef.current);
|
||||
}
|
||||
if (displayTimerRef.current) {
|
||||
cancelAnimationFrame(displayTimerRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
}, [isPdfDisplay, open, documentLink?.url]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsNativeScale(false);
|
||||
setNaturalSize({ width: null, height: null });
|
||||
focusRef.current = null;
|
||||
const scrollEl = scrollRef.current;
|
||||
if (scrollEl) {
|
||||
scrollEl.scrollLeft = 0;
|
||||
scrollEl.scrollTop = 0;
|
||||
}
|
||||
}, [open, documentLink?.url, displayKind]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !isNativeScale) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollEl = scrollRef.current;
|
||||
const mediaEl = mediaRef.current;
|
||||
if (!scrollEl || !mediaEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageWidth = naturalSize.width || mediaEl.clientWidth;
|
||||
const imageHeight = naturalSize.height || mediaEl.clientHeight;
|
||||
if (!(imageWidth > 0 && imageHeight > 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = focusRef.current || { xRatio: 0.5, yRatio: 0.5 };
|
||||
const maxScrollLeft = Math.max(0, imageWidth - scrollEl.clientWidth);
|
||||
const maxScrollTop = Math.max(0, imageHeight - scrollEl.clientHeight);
|
||||
|
||||
const desiredLeft = target.xRatio * imageWidth - scrollEl.clientWidth / 2;
|
||||
const desiredTop = target.yRatio * imageHeight - scrollEl.clientHeight / 2;
|
||||
|
||||
scrollEl.scrollLeft = clamp(desiredLeft, 0, maxScrollLeft);
|
||||
scrollEl.scrollTop = clamp(desiredTop, 0, maxScrollTop);
|
||||
}, [open, isNativeScale, naturalSize.width, naturalSize.height]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
previouslyFocusedRef.current?.focus?.();
|
||||
previouslyFocusedRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const active = document.activeElement;
|
||||
previouslyFocusedRef.current = active instanceof HTMLElement ? active : null;
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!documentLink?.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollEl = scrollRef.current;
|
||||
if (scrollEl) {
|
||||
scrollEl.scrollLeft = 0;
|
||||
scrollEl.scrollTop = 0;
|
||||
}
|
||||
focusRef.current = null;
|
||||
}, [documentLink?.url]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!renderBackdrop || !documentLink?.url) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const frame = requestAnimationFrame(() => {
|
||||
scrollRef.current?.focus?.({ preventScroll: true });
|
||||
});
|
||||
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, [renderBackdrop, documentLink?.url]);
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -213,59 +85,15 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
||||
|
||||
const key = event.key;
|
||||
|
||||
if (key === ' ' || key === 'Space' || key === 'Spacebar') {
|
||||
const target = event.target;
|
||||
if (target instanceof HTMLElement) {
|
||||
const tag = target.tagName ? target.tagName.toLowerCase() : '';
|
||||
if (
|
||||
target.isContentEditable
|
||||
|| tag === 'input'
|
||||
|| tag === 'textarea'
|
||||
|| tag === 'select'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (key === ' ' || key === 'Escape') {
|
||||
event.preventDefault();
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === 'Escape') {
|
||||
event.preventDefault();
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const toggleZoomAtPoint = (_clientX: number, _clientY: number) => {
|
||||
// Zooming is disabled for images as per user request.
|
||||
// PDFs handle their own zooming via the PdfViewer.
|
||||
return;
|
||||
};
|
||||
|
||||
const handleContentClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
if (isPdfDisplay) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
toggleZoomAtPoint(event.clientX ?? 0, event.clientY ?? 0);
|
||||
};
|
||||
|
||||
const shouldRender = renderBackdrop && Boolean(documentLink?.url);
|
||||
|
||||
const stageClassName = isPdfDisplay
|
||||
? 'preview-zoom__stage preview-zoom__stage--pdf'
|
||||
: 'preview-zoom__stage';
|
||||
|
||||
const containerClassName = [
|
||||
'preview-zoom__scroll',
|
||||
isNativeScale ? 'preview-zoom__scroll--native' : null,
|
||||
isPdfDisplay ? 'preview-zoom__scroll--pdf' : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
const stageClassName = 'preview-zoom__stage';
|
||||
const containerClassName = 'preview-zoom__scroll';
|
||||
|
||||
const backdropClassName = [
|
||||
'preview-zoom-backdrop',
|
||||
@@ -274,30 +102,10 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
const contentStyle: CSSProperties = isNativeScale
|
||||
? {
|
||||
cursor: 'default',
|
||||
width: naturalSize.width ? `${naturalSize.width}px` : 'auto',
|
||||
height: naturalSize.height ? `${naturalSize.height}px` : 'auto',
|
||||
maxWidth: 'none',
|
||||
maxHeight: 'none',
|
||||
touchAction: 'manipulation',
|
||||
}
|
||||
: {
|
||||
cursor: 'default',
|
||||
maxWidth: 'calc(100vw - 2 * var(--preview-padding, 1.5vmin))',
|
||||
maxHeight: 'calc(100vh - 2 * var(--preview-padding, 1.5vmin))',
|
||||
touchAction: 'manipulation',
|
||||
};
|
||||
|
||||
if (!shouldRender) {
|
||||
if (!renderBackdrop) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const effectiveDisplay = documentLink;
|
||||
|
||||
const downloadUrl = activeDocument?.current_version?.download?.url;
|
||||
|
||||
return createPortal(
|
||||
(
|
||||
<div
|
||||
@@ -364,41 +172,28 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
||||
ref={scrollRef}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{isPdfDisplay ? (
|
||||
<div className="preview-zoom__pdf">
|
||||
<PdfViewer
|
||||
src={effectiveDisplay.url}
|
||||
title={effectiveAlt}
|
||||
className="preview-zoom__pdf-viewer"
|
||||
viewportRef={scrollRef}
|
||||
/>
|
||||
</div>
|
||||
{displayKind === 'pdf' ? (
|
||||
<PdfViewer
|
||||
src={downloadUrl}
|
||||
title={documentTitle}
|
||||
className="preview-zoom__pdf-viewer"
|
||||
viewportRef={scrollRef}
|
||||
/>
|
||||
) : (
|
||||
<MediaViewer
|
||||
src={effectiveDisplay.url}
|
||||
alt={effectiveAlt}
|
||||
src={downloadUrl}
|
||||
alt={documentTitle}
|
||||
className="preview-zoom__image"
|
||||
mediaRef={(node: HTMLElement | null) => {
|
||||
mediaRef.current = node as HTMLImageElement;
|
||||
}}
|
||||
draggable={false}
|
||||
onLoad={(event) => {
|
||||
const target = event.currentTarget as HTMLImageElement;
|
||||
setNaturalSize({
|
||||
width: target.naturalWidth || null,
|
||||
height: target.naturalHeight || null,
|
||||
});
|
||||
}}
|
||||
onClick={handleContentClick}
|
||||
style={contentStyle}
|
||||
mimeType={effectiveDisplay.mimeType || undefined}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
mimeType={lastDocumentRef.current?.mime_type || undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
portalTarget,
|
||||
document.body,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import React, { createContext, useContext, useState, useCallback, useMemo, useRef } from 'react';
|
||||
import PreviewZoomOverlay from '../detail/PreviewZoomOverlay';
|
||||
import type { Document } from '../types/documents';
|
||||
import type { Identifier } from '../types/identifiers';
|
||||
@@ -20,21 +20,26 @@ export const usePreviewContext = () => {
|
||||
|
||||
interface PreviewProviderProps {
|
||||
children: React.ReactNode;
|
||||
ensureDownloadUrl?: (docId: Identifier) => Promise<any>;
|
||||
onNavigate?: (documentId: Identifier) => void;
|
||||
}
|
||||
|
||||
export const PreviewProvider: React.FC<PreviewProviderProps> = ({ children, ensureDownloadUrl, onNavigate }) => {
|
||||
export const PreviewProvider: React.FC<PreviewProviderProps> = ({ children, onNavigate }) => {
|
||||
const [previewDoc, setPreviewDoc] = useState<Document | null>(null);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
const lastFocusedElement = useRef<HTMLElement | null>(null);
|
||||
|
||||
const openPreview = useCallback((doc: Document) => {
|
||||
if (!lastFocusedElement.current) {
|
||||
lastFocusedElement.current = document.activeElement as HTMLElement;
|
||||
}
|
||||
setPreviewDoc(doc);
|
||||
}, []);
|
||||
|
||||
const closePreview = useCallback(() => {
|
||||
setPreviewDoc(null);
|
||||
setPreviewUrl(null);
|
||||
if (lastFocusedElement.current) {
|
||||
lastFocusedElement.current.focus();
|
||||
lastFocusedElement.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleMaximize = useCallback(() => {
|
||||
@@ -44,46 +49,6 @@ export const PreviewProvider: React.FC<PreviewProviderProps> = ({ children, ensu
|
||||
}
|
||||
}, [previewDoc, onNavigate, closePreview]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!previewDoc) {
|
||||
setPreviewUrl(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
if (ensureDownloadUrl) {
|
||||
ensureDownloadUrl(previewDoc.id)
|
||||
.then((entry) => {
|
||||
if (!cancelled && entry?.url) {
|
||||
setPreviewUrl(entry.url);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
setPreviewUrl(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [previewDoc, ensureDownloadUrl]);
|
||||
|
||||
const overlayDocument = useMemo(() => {
|
||||
if (!previewDoc || !previewUrl) {
|
||||
return previewDoc;
|
||||
}
|
||||
return {
|
||||
...previewDoc,
|
||||
documentLink: {
|
||||
url: previewUrl,
|
||||
alt: previewDoc.title,
|
||||
mimeType: previewDoc.mime_type,
|
||||
},
|
||||
};
|
||||
}, [previewDoc, previewUrl]);
|
||||
|
||||
const value = useMemo(() => ({
|
||||
openPreview,
|
||||
closePreview,
|
||||
@@ -93,10 +58,10 @@ export const PreviewProvider: React.FC<PreviewProviderProps> = ({ children, ensu
|
||||
<PreviewContext.Provider value={value}>
|
||||
{children}
|
||||
<PreviewZoomOverlay
|
||||
open={Boolean(previewUrl)}
|
||||
open={Boolean(previewDoc)}
|
||||
onClose={closePreview}
|
||||
onMaximize={onNavigate ? handleMaximize : undefined}
|
||||
document={overlayDocument}
|
||||
document={previewDoc}
|
||||
/>
|
||||
</PreviewContext.Provider>
|
||||
);
|
||||
|
||||
@@ -616,8 +616,6 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.document-summary__details {}
|
||||
|
||||
.document-summary__details-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
@@ -26,37 +26,32 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 6000001;
|
||||
--preview-padding: 1.5vmin;
|
||||
--header-clearance: calc(2.5rem + 0.5rem);
|
||||
}
|
||||
|
||||
.preview-zoom__stage--pdf {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
--preview-padding: 1.5vmin;
|
||||
--header-clearance: calc(2.5rem + 0.5rem);
|
||||
}
|
||||
|
||||
.preview-zoom__image {
|
||||
width: auto;
|
||||
height: auto;
|
||||
box-shadow: 0 32px 120px var(--shadow-deep);
|
||||
cursor: default;
|
||||
max-width: calc(100vw - 2 * var(--preview-padding, 1.5vmin));
|
||||
max-height: calc(100vh - var(--header-clearance) - 2 * var(--preview-padding, 1.5vmin));
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.preview-zoom__scroll {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: var(--header-clearance);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.preview-zoom__scroll--pdf {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
padding-top: var(--header-clearance);
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
--pdf-viewer-stack-padding: var(--preview-padding);
|
||||
}
|
||||
|
||||
@@ -64,25 +59,8 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.preview-zoom__scroll--native {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
cursor: zoom-out;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
|
||||
.preview-zoom__pdf {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding-top: var(--header-clearance);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.preview-zoom__pdf-viewer {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Identifier } from './identifiers';
|
||||
import type { Download } from './common';
|
||||
|
||||
export interface AssetObject {
|
||||
ordinal?: number;
|
||||
@@ -12,7 +13,7 @@ export interface AssetLike {
|
||||
id?: Identifier;
|
||||
asset_type?: string;
|
||||
cardinality?: number | null;
|
||||
download?: { url: string; expires_at: number } | null;
|
||||
download?: Download | null;
|
||||
metadata?: Record<string, unknown> | null;
|
||||
assets?: Record<string, AssetLike> | AssetLike[] | null;
|
||||
objects?: AssetObject[] | null;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface Download {
|
||||
url: string;
|
||||
expires_at: number;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Identifier } from './identifiers';
|
||||
import type { AssetLike } from './assets';
|
||||
import type { Download } from './common';
|
||||
|
||||
export interface DocumentTag {
|
||||
id?: Identifier;
|
||||
@@ -18,6 +19,7 @@ export interface DocumentVersion {
|
||||
metadata?: Record<string, unknown> & { page_count?: number } | null;
|
||||
size_bytes?: number | string | null;
|
||||
checksum?: string | null;
|
||||
download?: Download | null;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user