diff --git a/frontend/src/documents/DocumentThumbnailImage.tsx b/frontend/src/documents/DocumentThumbnailImage.tsx index e8c4813..7b28a16 100644 --- a/frontend/src/documents/DocumentThumbnailImage.tsx +++ b/frontend/src/documents/DocumentThumbnailImage.tsx @@ -73,12 +73,12 @@ const getPageCount = (doc?: Document | null): number | null => { type Asset = AssetManagerAsset; type EnsureAssetUrl = AssetManagerEnsureAssetUrl; -type GetDocumentAsset = AssetManagerGetAsset; +type GetAsset = AssetManagerGetAsset; interface DocumentThumbnailImageProps { document?: Document | null; ensureAssetUrl?: EnsureAssetUrl; - getDocumentAsset?: GetDocumentAsset; + getAsset?: GetAsset; alt?: string; maxSize?: number; scrollRootRef?: MutableRefObject | null; @@ -87,7 +87,7 @@ interface DocumentThumbnailImageProps { const DocumentThumbnailImage = ({ document, ensureAssetUrl, - getDocumentAsset, + getAsset, alt = '', maxSize = DEFAULT_THUMBNAIL_SIZE, scrollRootRef = null, @@ -127,16 +127,16 @@ const DocumentThumbnailImage = ({ } const options: { ensureAssetUrl?: EnsureAssetUrl; - getAsset?: GetDocumentAsset; + getAsset?: GetAsset; } = {}; if (ensureAssetUrl) { options.ensureAssetUrl = ensureAssetUrl; } - if (getDocumentAsset) { - options.getAsset = getDocumentAsset; + if (getAsset) { + options.getAsset = getAsset; } return resolveDocumentAssetUrl(document, 'thumbnail', options) || resolveAssetUrl(thumbnailAsset); - }, [document, ensureAssetUrl, getDocumentAsset, isVisible, thumbnailAsset]); + }, [document, ensureAssetUrl, getAsset, isVisible, thumbnailAsset]); const pageCount = getPageCount(document); const showMultiPageBadge = pageCount !== null && pageCount > 1; diff --git a/frontend/src/documents/components/DocumentsGridCard.tsx b/frontend/src/documents/components/DocumentsGridCard.tsx index cb4771d..8582af6 100644 --- a/frontend/src/documents/components/DocumentsGridCard.tsx +++ b/frontend/src/documents/components/DocumentsGridCard.tsx @@ -80,7 +80,7 @@ const DocumentsGridCard: React.FC = (props) => { = (props) => { false; - export const resolveDocumentDownloadHref = (document?: Document | null): string | null => { if (!document) { return null; @@ -21,65 +13,3 @@ export const resolveDocumentDownloadHref = (document?: Document | null): string } 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, - }; -}; diff --git a/frontend/src/utils/ocr.ts b/frontend/src/utils/ocr.ts deleted file mode 100644 index 4577943..0000000 --- a/frontend/src/utils/ocr.ts +++ /dev/null @@ -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; -export type EnsureAssetUrl = ( - id: string, - asset: Asset, - options?: { force?: boolean }, -) => Promise; -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 { - 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 { - 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; - } -} diff --git a/frontend/src/viewer/PdfViewer.tsx b/frontend/src/viewer/PdfViewer.tsx index e4730ad..3ce3b73 100644 --- a/frontend/src/viewer/PdfViewer.tsx +++ b/frontend/src/viewer/PdfViewer.tsx @@ -379,14 +379,17 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX. const stackStyle = useMemo(() => ({ '--pdf-viewer-viewport-width': `${viewportWidth}px`, '--pdf-viewer-viewport-height': `${viewportHeight}px`, - cursor: viewMode === 'fit-width' ? 'zoom-out' : 'zoom-in', - }), [viewportHeight, viewportWidth, viewMode]); + }), [viewportHeight, viewportWidth]); const toggleViewMode = useCallback(() => { setViewMode((prev) => (prev === 'fit-width' ? 'contain' : 'fit-width')); }, []); const handlePageClick = useCallback((event: React.MouseEvent) => { + if (!(event.target as HTMLElement).closest('.pdf-viewer__page-wrapper')) { + return; + } + const stack = containerRef.current; const viewportElement = viewportRef?.current || stack?.closest('.document-viewer__viewport'); if (stack && viewportElement) { diff --git a/frontend/src/viewer/UnifiedDocumentViewer.tsx b/frontend/src/viewer/UnifiedDocumentViewer.tsx index f630e4a..882e576 100644 --- a/frontend/src/viewer/UnifiedDocumentViewer.tsx +++ b/frontend/src/viewer/UnifiedDocumentViewer.tsx @@ -45,6 +45,7 @@ const UnifiedDocumentViewer: React.FC = ({ mimeType={normalizedMimeType} filename={normalizedFilename} alt={document.title} + onClick={(e) => e.stopPropagation()} /> ); }, [ diff --git a/frontend/src/viewer/components/PreviewZoomOverlay.tsx b/frontend/src/viewer/components/PreviewZoomOverlay.tsx index 334bfba..123549c 100644 --- a/frontend/src/viewer/components/PreviewZoomOverlay.tsx +++ b/frontend/src/viewer/components/PreviewZoomOverlay.tsx @@ -24,6 +24,7 @@ const PreviewZoomOverlay: React.FC = ({ const [isBackdropVisible, setBackdropVisible] = useState(false); const lastDocumentRef = useRef(null); const scrollRef = useRef(null); + const stageRef = useRef(null); const timerRef = useRef(null); if (inputDocument) { @@ -104,45 +105,43 @@ const PreviewZoomOverlay: React.FC = ({ onClick={onClose} onKeyDown={handleKeyDown} > - - - {onMaximize && ( +
e.stopPropagation()}> + - )} - - } - actions={ - <> - - - } - /> + {onMaximize && ( + + )} + + } + actions={ + <> + + + } + /> +
{ - if (event.target === event.currentTarget) { - onClose(); - } - }} + ref={stageRef} >
void; handleTagRemove?: (...args: unknown[]) => void; ensureAssetUrl?: EnsureAssetUrl; - getDocumentAsset?: GetDocumentAsset; + getAsset?: GetAsset; correspondents?: unknown[]; handleCorrespondentAdd?: (...args: unknown[]) => void; handleCorrespondentRemove?: (...args: unknown[]) => void; @@ -66,7 +66,7 @@ const useDetailWorkspace = ({ handleDocumentTagAdd, handleTagRemove, ensureAssetUrl, - getDocumentAsset, + getAsset, correspondents, handleCorrespondentAdd, handleCorrespondentRemove, @@ -191,9 +191,9 @@ const useDetailWorkspace = ({ (doc) => resolveDocumentAssetUrl(doc, 'thumbnail', { ensureAssetUrl, - getAsset: getDocumentAsset, + getAsset, }), - [ensureAssetUrl, getDocumentAsset], + [ensureAssetUrl, getAsset], ); const inspectDocument = useCallback( @@ -222,7 +222,7 @@ const useDetailWorkspace = ({ onUpdateTitle: handleDocumentTitleUpdate, onUpdateIssued: handleDocumentIssuedUpdate, ensureAssetUrl, - getDocumentAsset, + getDocumentAsset: getAsset, correspondents, onCorrespondentAdd: handleCorrespondentAdd, onCorrespondentRemove: handleCorrespondentRemove, @@ -237,7 +237,7 @@ const useDetailWorkspace = ({ correspondents, detailPanelDocument, ensureAssetUrl, - getDocumentAsset, + getAsset, handleCorrespondentAdd, handleCorrespondentRemove, handleDetailPanelClose, diff --git a/frontend/src/viewer/styles/viewer.css b/frontend/src/viewer/styles/viewer.css index e476243..b44feda 100644 --- a/frontend/src/viewer/styles/viewer.css +++ b/frontend/src/viewer/styles/viewer.css @@ -631,4 +631,12 @@ .pdf-viewer__password-error { color: var(--danger); 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; } \ No newline at end of file