This commit is contained in:
2025-12-04 23:55:04 +01:00
parent 86b9ac2ab6
commit 36c8e4cbaa
11 changed files with 62 additions and 254 deletions
@@ -73,12 +73,12 @@ const getPageCount = (doc?: Document | null): number | null => {
type Asset = AssetManagerAsset; type Asset = AssetManagerAsset;
type EnsureAssetUrl = AssetManagerEnsureAssetUrl; type EnsureAssetUrl = AssetManagerEnsureAssetUrl;
type GetDocumentAsset = AssetManagerGetAsset; type GetAsset = AssetManagerGetAsset;
interface DocumentThumbnailImageProps { interface DocumentThumbnailImageProps {
document?: Document | null; document?: Document | null;
ensureAssetUrl?: EnsureAssetUrl; ensureAssetUrl?: EnsureAssetUrl;
getDocumentAsset?: GetDocumentAsset; getAsset?: GetAsset;
alt?: string; alt?: string;
maxSize?: number; maxSize?: number;
scrollRootRef?: MutableRefObject<Element | null> | null; scrollRootRef?: MutableRefObject<Element | null> | null;
@@ -87,7 +87,7 @@ interface DocumentThumbnailImageProps {
const DocumentThumbnailImage = ({ const DocumentThumbnailImage = ({
document, document,
ensureAssetUrl, ensureAssetUrl,
getDocumentAsset, getAsset,
alt = '', alt = '',
maxSize = DEFAULT_THUMBNAIL_SIZE, maxSize = DEFAULT_THUMBNAIL_SIZE,
scrollRootRef = null, scrollRootRef = null,
@@ -127,16 +127,16 @@ const DocumentThumbnailImage = ({
} }
const options: { const options: {
ensureAssetUrl?: EnsureAssetUrl; ensureAssetUrl?: EnsureAssetUrl;
getAsset?: GetDocumentAsset; getAsset?: GetAsset;
} = {}; } = {};
if (ensureAssetUrl) { if (ensureAssetUrl) {
options.ensureAssetUrl = ensureAssetUrl; options.ensureAssetUrl = ensureAssetUrl;
} }
if (getDocumentAsset) { if (getAsset) {
options.getAsset = getDocumentAsset; options.getAsset = getAsset;
} }
return resolveDocumentAssetUrl(document, 'thumbnail', options) || resolveAssetUrl(thumbnailAsset); return resolveDocumentAssetUrl(document, 'thumbnail', options) || resolveAssetUrl(thumbnailAsset);
}, [document, ensureAssetUrl, getDocumentAsset, isVisible, thumbnailAsset]); }, [document, ensureAssetUrl, getAsset, isVisible, thumbnailAsset]);
const pageCount = getPageCount(document); const pageCount = getPageCount(document);
const showMultiPageBadge = pageCount !== null && pageCount > 1; const showMultiPageBadge = pageCount !== null && pageCount > 1;
@@ -80,7 +80,7 @@ const DocumentsGridCard: React.FC<DocumentsGridCardProps> = (props) => {
<DocumentThumbnailImage <DocumentThumbnailImage
document={doc} document={doc}
ensureAssetUrl={props.ensureAssetUrl} ensureAssetUrl={props.ensureAssetUrl}
getDocumentAsset={props.getDocumentAsset} getAsset={props.getDocumentAsset}
alt={`Thumbnail for ${doc.title}`} alt={`Thumbnail for ${doc.title}`}
maxSize={iconSize} maxSize={iconSize}
scrollRootRef={props.scrollRef} scrollRootRef={props.scrollRef}
@@ -90,7 +90,7 @@ const DocumentsListRow: React.FC<DocumentsListRowProps> = (props) => {
<DocumentThumbnailImage <DocumentThumbnailImage
document={doc} document={doc}
ensureAssetUrl={props.ensureAssetUrl} ensureAssetUrl={props.ensureAssetUrl}
getDocumentAsset={props.getDocumentAsset} getAsset={props.getDocumentAsset}
alt={`Thumbnail for ${doc.title}`} alt={`Thumbnail for ${doc.title}`}
scrollRootRef={props.scrollRef} scrollRootRef={props.scrollRef}
maxSize={props.iconSize} maxSize={props.iconSize}
@@ -1082,7 +1082,7 @@ const useDocumentsWorkspace = ({
handleDocumentTagAdd, handleDocumentTagAdd,
handleTagRemove, handleTagRemove,
ensureAssetUrl, ensureAssetUrl,
getDocumentAsset, getAsset: getDocumentAsset,
correspondents, correspondents,
handleCorrespondentAdd, handleCorrespondentAdd,
handleCorrespondentRemove, handleCorrespondentRemove,
-70
View File
@@ -1,13 +1,5 @@
import { openTextContentInNewTab } from '../utils/ocr';
import type {
EnsureAssetUrl,
EnsurePreviewData,
GetDocumentAsset,
} from '../utils/ocr';
import type { Document } from '../types/documents'; import type { Document } from '../types/documents';
const asyncFalse = async () => false;
export const resolveDocumentDownloadHref = (document?: Document | null): string | null => { export const resolveDocumentDownloadHref = (document?: Document | null): string | null => {
if (!document) { if (!document) {
return null; return null;
@@ -21,65 +13,3 @@ export const resolveDocumentDownloadHref = (document?: Document | null): string
} }
return download.url; return download.url;
}; };
const hasDocumentTextContentAsset = (document?: Document | null, getDocumentAsset?: GetDocumentAsset | null): boolean => {
if (!document || !getDocumentAsset) {
return false;
}
return Boolean(getDocumentAsset(document, 'text-content'));
};
interface CreateDocumentActionStateArgs {
document: Document | null;
ensurePreviewData: EnsurePreviewData;
ensureAssetUrl: EnsureAssetUrl;
getDocumentAsset?: GetDocumentAsset | null;
notifyApiError?: (error: unknown, message: string) => void;
ocrErrorMessage?: string;
}
export const createDocumentActionState = ({
document,
ensurePreviewData,
ensureAssetUrl,
getDocumentAsset,
notifyApiError,
ocrErrorMessage = 'Unable to open text content.',
}: CreateDocumentActionStateArgs) => {
if (!document) {
return {
downloadHref: null,
hasOcr: false,
openOcr: asyncFalse,
};
}
const downloadHref = resolveDocumentDownloadHref(document);
const hasOcr = hasDocumentTextContentAsset(document, getDocumentAsset);
const openOcr = hasOcr
? async () => {
try {
const success = await openTextContentInNewTab({
document,
ensurePreviewData,
getDocumentAsset,
ensureAssetUrl,
});
if (!success) {
notifyApiError?.(new Error('Text content URL unavailable.'), ocrErrorMessage);
}
return success;
} catch (error) {
notifyApiError?.(error, ocrErrorMessage);
throw error;
}
}
: asyncFalse;
return {
downloadHref,
hasOcr,
openOcr,
};
};
-133
View File
@@ -1,133 +0,0 @@
import { resolveDocumentAssetUrl, resolveAssetUrl } from '../lib/assets/AssetManager';
import type {
GetAsset as AssetManagerGetAsset,
} from '../lib/assets/AssetManager';
import type {
Document,
} from '../types/documents';
import type { Asset } from '../types/assets';
export type EnsurePreviewData = (id: string) => Promise<Document | null>;
export type EnsureAssetUrl = (
id: string,
asset: Asset,
options?: { force?: boolean },
) => Promise<Asset | null>;
export type GetDocumentAsset = AssetManagerGetAsset;
interface ResolveTextContentUrlOptions {
document: Document | null;
ensurePreviewData?: EnsurePreviewData;
getDocumentAsset?: GetDocumentAsset;
ensureAssetUrl?: EnsureAssetUrl;
}
const pickAsset = (doc?: Document | null, getDocumentAsset?: GetDocumentAsset): Asset | null => {
if (!doc || !getDocumentAsset) {
return null;
}
return getDocumentAsset(doc, 'text-content') || null;
};
async function resolveTextContentUrl({
document,
ensurePreviewData,
getDocumentAsset,
ensureAssetUrl,
}: ResolveTextContentUrlOptions): Promise<string | null> {
if (!document?.id) {
return null;
}
let docRef = document;
let asset = pickAsset(docRef, getDocumentAsset);
if (!asset && ensurePreviewData) {
const refreshed = await ensurePreviewData(docRef.id);
if (refreshed) {
docRef = refreshed;
}
asset = pickAsset(docRef, getDocumentAsset);
}
if (!asset) {
return null;
}
const baseUrl = resolveAssetUrl(asset);
const hasUrl = Boolean(baseUrl);
let entry: Asset = asset;
if (ensureAssetUrl) {
const ensureOptions = { force: !hasUrl };
const ensured = await ensureAssetUrl(docRef.id!, asset, ensureOptions);
if (ensured) {
entry = ensured;
}
}
const directUrl = resolveAssetUrl(entry);
if (directUrl) {
return directUrl;
}
return (
resolveDocumentAssetUrl(docRef, 'text-content', {
ensureAssetUrl,
getAsset: getDocumentAsset,
ensureOptions: { force: true },
}) || null
);
}
export async function openTextContentInNewTab(options: ResolveTextContentUrlOptions): Promise<boolean> {
const popup = window.open('', '_blank');
const popupAvailable = Boolean(popup);
if (popupAvailable) {
try {
popup.opener = null;
} catch {
/* ignore */
}
}
try {
const url = await resolveTextContentUrl(options);
if (!url) {
if (popupAvailable) {
popup.close();
}
return false;
}
if (popupAvailable) {
try {
popup.location.replace(url);
} catch {
try {
popup.location.href = url;
} catch (secondError) {
popup.close();
throw secondError;
}
}
} else {
const finalWindow = window.open(url, '_blank');
if (finalWindow) {
try {
finalWindow.opener = null;
} catch {
/* ignore */
}
}
}
return true;
} catch (error) {
if (popupAvailable) {
popup.close();
}
throw error;
}
}
+5 -2
View File
@@ -379,14 +379,17 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
const stackStyle = useMemo<CSSProperties>(() => ({ const stackStyle = useMemo<CSSProperties>(() => ({
'--pdf-viewer-viewport-width': `${viewportWidth}px`, '--pdf-viewer-viewport-width': `${viewportWidth}px`,
'--pdf-viewer-viewport-height': `${viewportHeight}px`, '--pdf-viewer-viewport-height': `${viewportHeight}px`,
cursor: viewMode === 'fit-width' ? 'zoom-out' : 'zoom-in', }), [viewportHeight, viewportWidth]);
}), [viewportHeight, viewportWidth, viewMode]);
const toggleViewMode = useCallback(() => { const toggleViewMode = useCallback(() => {
setViewMode((prev) => (prev === 'fit-width' ? 'contain' : 'fit-width')); setViewMode((prev) => (prev === 'fit-width' ? 'contain' : 'fit-width'));
}, []); }, []);
const handlePageClick = useCallback((event: React.MouseEvent<HTMLDivElement>) => { const handlePageClick = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
if (!(event.target as HTMLElement).closest('.pdf-viewer__page-wrapper')) {
return;
}
const stack = containerRef.current; const stack = containerRef.current;
const viewportElement = viewportRef?.current || stack?.closest('.document-viewer__viewport'); const viewportElement = viewportRef?.current || stack?.closest('.document-viewer__viewport');
if (stack && viewportElement) { if (stack && viewportElement) {
@@ -45,6 +45,7 @@ const UnifiedDocumentViewer: React.FC<UnifiedDocumentViewerProps> = ({
mimeType={normalizedMimeType} mimeType={normalizedMimeType}
filename={normalizedFilename} filename={normalizedFilename}
alt={document.title} alt={document.title}
onClick={(e) => e.stopPropagation()}
/> />
); );
}, [ }, [
@@ -24,6 +24,7 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
const [isBackdropVisible, setBackdropVisible] = useState(false); const [isBackdropVisible, setBackdropVisible] = useState(false);
const lastDocumentRef = useRef<Document | null>(null); const lastDocumentRef = useRef<Document | null>(null);
const scrollRef = useRef<HTMLDivElement | null>(null); const scrollRef = useRef<HTMLDivElement | null>(null);
const stageRef = useRef<HTMLDivElement | null>(null);
const timerRef = useRef<number | null>(null); const timerRef = useRef<number | null>(null);
if (inputDocument) { if (inputDocument) {
@@ -104,45 +105,43 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
onClick={onClose} onClick={onClose}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
> >
<PanelHeader <div onClick={(e) => e.stopPropagation()}>
className="panel-header--dark" <PanelHeader
title={documentTitle} className="panel-header--dark"
leading={ title={documentTitle}
<> leading={
<button <>
onClick={onClose}
className="icon-button"
aria-label="Close preview"
type="button"
>
<IconX />
</button>
{onMaximize && (
<button <button
onClick={onMaximize} onClick={onClose}
className="icon-button" className="icon-button"
aria-label="Open document info" aria-label="Close preview"
title="Open document info"
type="button" type="button"
> >
<FileInfoIcon /> <IconX />
</button> </button>
)} {onMaximize && (
</> <button
} onClick={onMaximize}
actions={ className="icon-button"
<> aria-label="Open document info"
<DocumentDownloadLink document={lastDocumentRef.current} /> title="Open document info"
</> type="button"
} >
/> <FileInfoIcon />
</button>
)}
</>
}
actions={
<>
<DocumentDownloadLink document={lastDocumentRef.current} />
</>
}
/>
</div>
<div <div
className={stageClassName} className={stageClassName}
onClick={(event) => { ref={stageRef}
if (event.target === event.currentTarget) {
onClose();
}
}}
> >
<div <div
className={containerClassName} className={containerClassName}
@@ -4,7 +4,7 @@ import { resolveDocumentAssetUrl } from '../../lib/assets/AssetManager';
import { useDetailPanel } from '../../app/useDetailPanel'; import { useDetailPanel } from '../../app/useDetailPanel';
import { DEFAULT_FOLDER_NAME } from '../../app/workspaceUtils'; import { DEFAULT_FOLDER_NAME } from '../../app/workspaceUtils';
import type { DocumentInfoPanelProps } from '../components/DocumentInfoPanel'; import type { DocumentInfoPanelProps } from '../components/DocumentInfoPanel';
import type { EnsureAssetUrl, GetDocumentAsset } from '../../utils/ocr'; import type { EnsureAssetUrl, GetAsset } from '../../lib/assets/AssetManager';
import type { Identifier } from '../../types/identifiers'; import type { Identifier } from '../../types/identifiers';
import type { Document } from '../../types/documents'; import type { Document } from '../../types/documents';
@@ -29,7 +29,7 @@ interface UseDetailWorkspaceArgs {
handleDocumentTagAdd?: (doc: Document, value: string, context?: { option?: unknown }) => void; handleDocumentTagAdd?: (doc: Document, value: string, context?: { option?: unknown }) => void;
handleTagRemove?: (...args: unknown[]) => void; handleTagRemove?: (...args: unknown[]) => void;
ensureAssetUrl?: EnsureAssetUrl; ensureAssetUrl?: EnsureAssetUrl;
getDocumentAsset?: GetDocumentAsset; getAsset?: GetAsset;
correspondents?: unknown[]; correspondents?: unknown[];
handleCorrespondentAdd?: (...args: unknown[]) => void; handleCorrespondentAdd?: (...args: unknown[]) => void;
handleCorrespondentRemove?: (...args: unknown[]) => void; handleCorrespondentRemove?: (...args: unknown[]) => void;
@@ -66,7 +66,7 @@ const useDetailWorkspace = ({
handleDocumentTagAdd, handleDocumentTagAdd,
handleTagRemove, handleTagRemove,
ensureAssetUrl, ensureAssetUrl,
getDocumentAsset, getAsset,
correspondents, correspondents,
handleCorrespondentAdd, handleCorrespondentAdd,
handleCorrespondentRemove, handleCorrespondentRemove,
@@ -191,9 +191,9 @@ const useDetailWorkspace = ({
(doc) => (doc) =>
resolveDocumentAssetUrl(doc, 'thumbnail', { resolveDocumentAssetUrl(doc, 'thumbnail', {
ensureAssetUrl, ensureAssetUrl,
getAsset: getDocumentAsset, getAsset,
}), }),
[ensureAssetUrl, getDocumentAsset], [ensureAssetUrl, getAsset],
); );
const inspectDocument = useCallback( const inspectDocument = useCallback(
@@ -222,7 +222,7 @@ const useDetailWorkspace = ({
onUpdateTitle: handleDocumentTitleUpdate, onUpdateTitle: handleDocumentTitleUpdate,
onUpdateIssued: handleDocumentIssuedUpdate, onUpdateIssued: handleDocumentIssuedUpdate,
ensureAssetUrl, ensureAssetUrl,
getDocumentAsset, getDocumentAsset: getAsset,
correspondents, correspondents,
onCorrespondentAdd: handleCorrespondentAdd, onCorrespondentAdd: handleCorrespondentAdd,
onCorrespondentRemove: handleCorrespondentRemove, onCorrespondentRemove: handleCorrespondentRemove,
@@ -237,7 +237,7 @@ const useDetailWorkspace = ({
correspondents, correspondents,
detailPanelDocument, detailPanelDocument,
ensureAssetUrl, ensureAssetUrl,
getDocumentAsset, getAsset,
handleCorrespondentAdd, handleCorrespondentAdd,
handleCorrespondentRemove, handleCorrespondentRemove,
handleDetailPanelClose, handleDetailPanelClose,
+8
View File
@@ -631,4 +631,12 @@
.pdf-viewer__password-error { .pdf-viewer__password-error {
color: var(--danger); color: var(--danger);
font-size: 0.9rem; font-size: 0.9rem;
}
.pdf-viewer__canvas-stack--fit-width .pdf-viewer__page-wrapper {
cursor: zoom-out;
}
.pdf-viewer__canvas-stack--contain .pdf-viewer__page-wrapper {
cursor: zoom-in;
} }