diff --git a/apps/compliance-portal/src/pages/documents/DocumentViewerPage.tsx b/apps/compliance-portal/src/pages/documents/DocumentViewerPage.tsx index 73525c883..630c25af5 100644 --- a/apps/compliance-portal/src/pages/documents/DocumentViewerPage.tsx +++ b/apps/compliance-portal/src/pages/documents/DocumentViewerPage.tsx @@ -22,7 +22,6 @@ import type { PreloadedQuery } from "react-relay"; import { graphql, usePreloadedQuery } from "react-relay"; import type { DocumentViewerPageQuery } from "./__generated__/DocumentViewerPageQuery.graphql"; -import { DocumentLocked } from "./_components/DocumentLocked"; import { DocumentViewer } from "./_components/DocumentViewer"; import { useAccessRequest } from "./_lib/useAccessRequest"; import type { DocumentKind } from "./_lib/useDocumentExport"; @@ -76,8 +75,8 @@ function resolveNode(node: DocumentViewerPageQuery["response"]["aliasedNode"]): } // Full-page viewer for a single document/file/report resolved by its alias. It -// exports the (watermarked) bytes and renders them; unauthorized visitors get a -// locked state instead. +// exports the (watermarked) bytes and renders them; unauthorized visitors keep +// the title header and see a locked empty state in place of the preview. export function DocumentViewerPage({ queryRef }: DocumentViewerPageProps) { const data = usePreloadedQuery(documentViewerPageQuery, queryRef); const node = resolveNode(data.aliasedNode); @@ -85,7 +84,12 @@ export function DocumentViewerPage({ queryRef }: DocumentViewerPageProps) { const { requestAccess, isRequesting } = useAccessRequest(node.kind, node.id); if (!node.isAuthorized) { - return ; + return ( + + ); } return ; diff --git a/apps/compliance-portal/src/pages/documents/_components/DocumentViewer.tsx b/apps/compliance-portal/src/pages/documents/_components/DocumentViewer.tsx index 3eb59ec32..eb0d7ab48 100644 --- a/apps/compliance-portal/src/pages/documents/_components/DocumentViewer.tsx +++ b/apps/compliance-portal/src/pages/documents/_components/DocumentViewer.tsx @@ -43,6 +43,7 @@ import { useLocalizedPath } from "#/lib/i18n/useLocale"; import { dataUriMimeType, downloadDataUri } from "../_lib/dataUri"; import { DocumentDownloadFallback } from "./DocumentDownloadFallback"; +import { DocumentLocked } from "./DocumentLocked"; import type { PdfPreviewHandle } from "./PdfPreview"; import { PdfPreview } from "./PdfPreview"; import { documentViewer } from "./variants"; @@ -54,20 +55,33 @@ function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } +interface DocumentViewerLocked { + // Requests access for the locked resource (prompting sign-in first when + // needed). + onGetAccess: () => void; + // Whether the access request is in flight. + isRequesting: boolean; +} + interface DocumentViewerProps { // The document/file/report display name. title: string; - // The exported base64 data URI, or null while it is still loading. - dataUri: string | null; - // File name used when downloading. - downloadName: string; + // The exported base64 data URI, or null while it is still loading. Omitted + // when the viewer is locked. + dataUri?: string | null; + // File name used when downloading. Omitted when the viewer is locked. + downloadName?: string; + // When set, the header keeps the title (no toolbar) and the body shows the + // locked empty state with a Get Access CTA instead of the file preview. + locked?: DocumentViewerLocked; } // Full-page document viewer: a header band with the title and a toolbar // (page navigation + zoom for PDFs, copy link, download) above the scrollable // body. PDFs render with react-pdf, images inline, and anything else offers a -// download. -export function DocumentViewer({ title, dataUri, downloadName }: DocumentViewerProps) { +// download. Locked visitors keep the title header and see a Get Access CTA in +// place of the preview. +export function DocumentViewer({ title, dataUri = null, downloadName = title, locked }: DocumentViewerProps) { const { t } = useTranslation("documents"); const toast = Toast.useToastManager(); const localizedPath = useLocalizedPath(); @@ -77,7 +91,8 @@ export function DocumentViewer({ title, dataUri, downloadName }: DocumentViewerP const [currentPage, setCurrentPage] = useState(1); const [scale, setScale] = useState(1); - const mimeType = dataUri ? dataUriMimeType(dataUri) : null; + const isLocked = locked != null; + const mimeType = !isLocked && dataUri ? dataUriMimeType(dataUri) : null; const isPdf = mimeType === "application/pdf"; const isImage = mimeType?.startsWith("image/") ?? false; @@ -104,7 +119,7 @@ export function DocumentViewer({ title, dataUri, downloadName }: DocumentViewerP return (
- +
} className={slots.back()}> {t("viewer.back")} @@ -112,108 +127,117 @@ export function DocumentViewer({ title, dataUri, downloadName }: DocumentViewerP {title} -
-
- {isPdf && ( - <> -
- movePage(-1)} - > - - - - {t("common.pageOf", { current: currentPage, total: numPages })} - - = numPages} - onClick={() => movePage(1)} - > - - -
- -
- setScale(value => clamp(value * 0.8, MIN_SCALE, MAX_SCALE))} - > - - - - {`${Math.round(scale * 100)}%`} - - setScale(value => clamp(value * 1.25, MIN_SCALE, MAX_SCALE))} - > - - -
- - )} + {!isLocked && ( +
+
+ {isPdf && ( + <> +
+ movePage(-1)} + > + + + + {t("common.pageOf", { current: currentPage, total: numPages })} + + = numPages} + onClick={() => movePage(1)} + > + + +
+ +
+ setScale(value => clamp(value * 0.8, MIN_SCALE, MAX_SCALE))} + > + + + + {`${Math.round(scale * 100)}%`} + + setScale(value => clamp(value * 1.25, MIN_SCALE, MAX_SCALE))} + > + + +
+ + )} +
+
+ + + +
-
- - - -
-
+ )}
- {dataUri == null + {isLocked ? ( -
- -
+ ) - : isPdf + : dataUri == null ? ( - +
+ +
) - : isImage + : isPdf ? ( -
- {title} -
+ ) - : } + : isImage + ? ( +
+ {title} +
+ ) + : }
);