feat: Add document download link component, refactor download URL resolution with expiration checks
This commit is contained in:
@@ -2,6 +2,7 @@ import { useCallback, useMemo, useRef } from 'react';
|
||||
import type { JSX } from 'react';
|
||||
import DocumentInfoPanel from './components/DocumentInfoPanel';
|
||||
import UnifiedDocumentViewer from './UnifiedDocumentViewer';
|
||||
import { resolveDocumentDownloadHref } from '../documents/documentActions';
|
||||
|
||||
import type { Document } from '../types/documents';
|
||||
|
||||
@@ -56,13 +57,13 @@ const DocumentViewerLayout = ({
|
||||
|
||||
const renderViewportPane = useCallback(() => (
|
||||
<div className="document-viewer__viewport" ref={viewportRef}>
|
||||
{!document?.current_version?.download?.url ? (
|
||||
{!resolveDocumentDownloadHref(document) ? (
|
||||
<div className="document-viewer__message">{previewLoadingMessage}</div>
|
||||
) : (
|
||||
previewContent
|
||||
)}
|
||||
</div>
|
||||
), [previewContent, document?.current_version?.download?.url, previewLoadingMessage, viewportRef]);
|
||||
), [previewContent, document, previewLoadingMessage, viewportRef]);
|
||||
|
||||
const viewportPane = renderViewportPane();
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
DownloadIcon,
|
||||
CloseIcon,
|
||||
IconZoomInArea,
|
||||
WindowMaximizeIcon,
|
||||
@@ -15,10 +15,10 @@ import {
|
||||
buildCorrespondentOptions,
|
||||
sortCorrespondents,
|
||||
} from './components/DocumentSummarySection';
|
||||
import DocumentDownloadLink from '../documents/components/DocumentDownloadLink';
|
||||
import { usePreviewContext } from './PreviewContext';
|
||||
import type { DocumentSummarySectionProps } from './components/DocumentSummarySection';
|
||||
import { extractDocumentMetadataPayload } from './logic/documentSummary';
|
||||
import { createDocumentActionState } from '../documents/documentActions';
|
||||
import { resolveDocumentAssetUrl } from '../lib/assets/AssetManager';
|
||||
import PanelHeader from '../components/PanelHeader';
|
||||
import BreadcrumbTrail from '../components/BreadcrumbTrail';
|
||||
@@ -48,33 +48,14 @@ interface DocumentViewerPanelProps extends DocumentSummarySectionProps {
|
||||
|
||||
const createDocumentViewerHeaderActions = ({
|
||||
document,
|
||||
actionState,
|
||||
onZoom,
|
||||
canZoom = false,
|
||||
}) => {
|
||||
if (!document) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const downloadHref = actionState?.downloadHref;
|
||||
if (!downloadHref && !(canZoom && onZoom)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{downloadHref ? (
|
||||
<a
|
||||
className="icon-button"
|
||||
href={downloadHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Download document"
|
||||
title="Download document"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</a>
|
||||
) : null}
|
||||
<DocumentDownloadLink document={document} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -93,7 +74,6 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
||||
ensureAssetUrl,
|
||||
getDocumentAsset,
|
||||
ensurePreviewData,
|
||||
notifyApiError,
|
||||
sidebarToggle = null,
|
||||
onClose,
|
||||
resolveFolderPath,
|
||||
@@ -125,6 +105,18 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
||||
return Boolean(getDocumentAsset(document, 'text-content'));
|
||||
}, [document, getDocumentAsset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!document?.id || !ensurePreviewData) {
|
||||
return;
|
||||
}
|
||||
const download = document.current_version?.download;
|
||||
if (download?.expires_at && download.expires_at <= Date.now()) {
|
||||
ensurePreviewData(document.id).catch((error) => {
|
||||
console.warn('Failed to refresh expired document', error);
|
||||
});
|
||||
}
|
||||
}, [document, ensurePreviewData]);
|
||||
|
||||
const navigateToFolder = useCallback(
|
||||
(folderId: FolderId | null) => {
|
||||
const target = folderId == null
|
||||
@@ -242,27 +234,6 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
||||
? 'document-viewer document-viewer--stacked'
|
||||
: 'document-viewer';
|
||||
|
||||
const actionState = useMemo(
|
||||
() =>
|
||||
document
|
||||
? createDocumentActionState({
|
||||
document,
|
||||
ensurePreviewData,
|
||||
ensureAssetUrl,
|
||||
getDocumentAsset,
|
||||
notifyApiError,
|
||||
ocrErrorMessage: 'Unable to open text content.',
|
||||
})
|
||||
: null,
|
||||
[
|
||||
document,
|
||||
ensurePreviewData,
|
||||
ensureAssetUrl,
|
||||
getDocumentAsset,
|
||||
notifyApiError,
|
||||
],
|
||||
);
|
||||
|
||||
const breadcrumbs = useMemo(() => {
|
||||
if (!document || !resolveFolderPath) {
|
||||
return [];
|
||||
@@ -294,9 +265,6 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
||||
|
||||
const headerActions = createDocumentViewerHeaderActions({
|
||||
document,
|
||||
actionState,
|
||||
onZoom: handleZoomOpen,
|
||||
canZoom: Boolean(document),
|
||||
});
|
||||
|
||||
const maximizeButton = isSidebarVariant && onMaximize
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import PdfViewer from './PdfViewer';
|
||||
import MediaViewer from './MediaViewer';
|
||||
import { resolveDocumentDownloadHref } from '../documents/documentActions';
|
||||
import type { Document } from '../types/documents';
|
||||
|
||||
interface UnifiedDocumentViewerProps {
|
||||
@@ -17,7 +18,7 @@ const UnifiedDocumentViewer: React.FC<UnifiedDocumentViewerProps> = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const downloadUrl = document.current_version?.download?.url;
|
||||
const downloadUrl = resolveDocumentDownloadHref(document);
|
||||
if (!downloadUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import UnifiedDocumentViewer from '../UnifiedDocumentViewer';
|
||||
import DocumentDownloadLink from '../../documents/components/DocumentDownloadLink';
|
||||
import PanelHeader from '../../components/PanelHeader';
|
||||
import { IconX, DownloadIcon, FileInfoIcon } from '../../components/icons';
|
||||
import { IconX, FileInfoIcon } from '../../components/icons';
|
||||
|
||||
import type { Document } from '../../types/documents';
|
||||
|
||||
@@ -29,7 +30,6 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
||||
lastDocumentRef.current = inputDocument;
|
||||
}
|
||||
|
||||
const downloadUrl = lastDocumentRef.current?.current_version?.download?.url;
|
||||
const documentTitle = lastDocumentRef.current?.title || undefined;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -132,18 +132,7 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
{downloadUrl && (
|
||||
<a
|
||||
href={downloadUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="icon-button"
|
||||
aria-label="Download document"
|
||||
title="Download document"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</a>
|
||||
)}
|
||||
<DocumentDownloadLink document={lastDocumentRef.current} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
.preview-zoom-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: var(--overlay-backdrop);
|
||||
transition: background 0.25s ease, opacity 0.25s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 6000000;
|
||||
cursor: default;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.preview-zoom-backdrop--visible {
|
||||
opacity: 1;
|
||||
background: var(--overlay-backdrop);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.preview-zoom__stage {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 6000001;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
--preview-padding: 1.5vmin;
|
||||
--header-clearance: calc(2.5rem + 0.5rem);
|
||||
}
|
||||
|
||||
.preview-zoom__scroll .document-viewer__object--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;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
padding-top: var(--header-clearance);
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
--pdf-viewer-stack-padding: var(--preview-padding);
|
||||
}
|
||||
|
||||
.preview-zoom__scroll:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.preview-zoom__scroll .document-viewer__object--pdf {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-zoom__stage .pdf-viewer__page-wrapper {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.preview-zoom__stage .pdf-viewer__page-wrapper::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
box-shadow: 0 32px 120px var(--shadow-deep);
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
/* Header positioning for preview zoom overlay */
|
||||
.preview-zoom-backdrop .panel-header--dark {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 6000002;
|
||||
cursor: default;
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
pointer-events: auto;
|
||||
box-shadow: 0 1rem 0.5rem var(--shadow-subtle);
|
||||
}
|
||||
|
||||
/* Prevent header clicks from closing backdrop */
|
||||
.preview-zoom-backdrop .panel-header--dark * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
Reference in New Issue
Block a user