Keep document title on locked viewer
Unauthorized visitors still need the document identity and a way back to the list; only the preview toolbar is withheld, with the access CTA replacing the file body. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -22,7 +22,6 @@ import type { PreloadedQuery } from "react-relay";
|
|||||||
import { graphql, usePreloadedQuery } from "react-relay";
|
import { graphql, usePreloadedQuery } from "react-relay";
|
||||||
|
|
||||||
import type { DocumentViewerPageQuery } from "./__generated__/DocumentViewerPageQuery.graphql";
|
import type { DocumentViewerPageQuery } from "./__generated__/DocumentViewerPageQuery.graphql";
|
||||||
import { DocumentLocked } from "./_components/DocumentLocked";
|
|
||||||
import { DocumentViewer } from "./_components/DocumentViewer";
|
import { DocumentViewer } from "./_components/DocumentViewer";
|
||||||
import { useAccessRequest } from "./_lib/useAccessRequest";
|
import { useAccessRequest } from "./_lib/useAccessRequest";
|
||||||
import type { DocumentKind } from "./_lib/useDocumentExport";
|
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
|
// 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
|
// exports the (watermarked) bytes and renders them; unauthorized visitors keep
|
||||||
// locked state instead.
|
// the title header and see a locked empty state in place of the preview.
|
||||||
export function DocumentViewerPage({ queryRef }: DocumentViewerPageProps) {
|
export function DocumentViewerPage({ queryRef }: DocumentViewerPageProps) {
|
||||||
const data = usePreloadedQuery<DocumentViewerPageQuery>(documentViewerPageQuery, queryRef);
|
const data = usePreloadedQuery<DocumentViewerPageQuery>(documentViewerPageQuery, queryRef);
|
||||||
const node = resolveNode(data.aliasedNode);
|
const node = resolveNode(data.aliasedNode);
|
||||||
@@ -85,7 +84,12 @@ export function DocumentViewerPage({ queryRef }: DocumentViewerPageProps) {
|
|||||||
const { requestAccess, isRequesting } = useAccessRequest(node.kind, node.id);
|
const { requestAccess, isRequesting } = useAccessRequest(node.kind, node.id);
|
||||||
|
|
||||||
if (!node.isAuthorized) {
|
if (!node.isAuthorized) {
|
||||||
return <DocumentLocked onGetAccess={requestAccess} isRequesting={isRequesting} />;
|
return (
|
||||||
|
<DocumentViewer
|
||||||
|
title={node.title}
|
||||||
|
locked={{ onGetAccess: requestAccess, isRequesting }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <DocumentViewer title={node.title} dataUri={dataUri} downloadName={node.title} />;
|
return <DocumentViewer title={node.title} dataUri={dataUri} downloadName={node.title} />;
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import { useLocalizedPath } from "#/lib/i18n/useLocale";
|
|||||||
import { dataUriMimeType, downloadDataUri } from "../_lib/dataUri";
|
import { dataUriMimeType, downloadDataUri } from "../_lib/dataUri";
|
||||||
|
|
||||||
import { DocumentDownloadFallback } from "./DocumentDownloadFallback";
|
import { DocumentDownloadFallback } from "./DocumentDownloadFallback";
|
||||||
|
import { DocumentLocked } from "./DocumentLocked";
|
||||||
import type { PdfPreviewHandle } from "./PdfPreview";
|
import type { PdfPreviewHandle } from "./PdfPreview";
|
||||||
import { PdfPreview } from "./PdfPreview";
|
import { PdfPreview } from "./PdfPreview";
|
||||||
import { documentViewer } from "./variants";
|
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);
|
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 {
|
interface DocumentViewerProps {
|
||||||
// The document/file/report display name.
|
// The document/file/report display name.
|
||||||
title: string;
|
title: string;
|
||||||
// The exported base64 data URI, or null while it is still loading.
|
// The exported base64 data URI, or null while it is still loading. Omitted
|
||||||
dataUri: string | null;
|
// when the viewer is locked.
|
||||||
// File name used when downloading.
|
dataUri?: string | null;
|
||||||
downloadName: string;
|
// 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
|
// Full-page document viewer: a header band with the title and a toolbar
|
||||||
// (page navigation + zoom for PDFs, copy link, download) above the scrollable
|
// (page navigation + zoom for PDFs, copy link, download) above the scrollable
|
||||||
// body. PDFs render with react-pdf, images inline, and anything else offers a
|
// body. PDFs render with react-pdf, images inline, and anything else offers a
|
||||||
// download.
|
// download. Locked visitors keep the title header and see a Get Access CTA in
|
||||||
export function DocumentViewer({ title, dataUri, downloadName }: DocumentViewerProps) {
|
// place of the preview.
|
||||||
|
export function DocumentViewer({ title, dataUri = null, downloadName = title, locked }: DocumentViewerProps) {
|
||||||
const { t } = useTranslation("documents");
|
const { t } = useTranslation("documents");
|
||||||
const toast = Toast.useToastManager();
|
const toast = Toast.useToastManager();
|
||||||
const localizedPath = useLocalizedPath();
|
const localizedPath = useLocalizedPath();
|
||||||
@@ -77,7 +91,8 @@ export function DocumentViewer({ title, dataUri, downloadName }: DocumentViewerP
|
|||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [scale, setScale] = 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 isPdf = mimeType === "application/pdf";
|
||||||
const isImage = mimeType?.startsWith("image/") ?? false;
|
const isImage = mimeType?.startsWith("image/") ?? false;
|
||||||
|
|
||||||
@@ -104,7 +119,7 @@ export function DocumentViewer({ title, dataUri, downloadName }: DocumentViewerP
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={slots.root()}>
|
<div className={slots.root()}>
|
||||||
<HeaderBand flushBottomSpace>
|
<HeaderBand flushBottomSpace={!isLocked}>
|
||||||
<div className={slots.header()}>
|
<div className={slots.header()}>
|
||||||
<Link to={localizedPath("/documents")} variant="ghost" color="neutral" size={1} iconStart={<CaretLeftIcon />} className={slots.back()}>
|
<Link to={localizedPath("/documents")} variant="ghost" color="neutral" size={1} iconStart={<CaretLeftIcon />} className={slots.back()}>
|
||||||
{t("viewer.back")}
|
{t("viewer.back")}
|
||||||
@@ -112,6 +127,7 @@ export function DocumentViewer({ title, dataUri, downloadName }: DocumentViewerP
|
|||||||
<Heading level={1} size={7} weight="medium" highContrast className="truncate">
|
<Heading level={1} size={7} weight="medium" highContrast className="truncate">
|
||||||
{title}
|
{title}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
{!isLocked && (
|
||||||
<div className={slots.toolbar()}>
|
<div className={slots.toolbar()}>
|
||||||
<div className={slots.toolbarStart()}>
|
<div className={slots.toolbarStart()}>
|
||||||
{isPdf && (
|
{isPdf && (
|
||||||
@@ -187,11 +203,19 @@ export function DocumentViewer({ title, dataUri, downloadName }: DocumentViewerP
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</HeaderBand>
|
</HeaderBand>
|
||||||
|
|
||||||
<div className={slots.body()}>
|
<div className={slots.body()}>
|
||||||
{dataUri == null
|
{isLocked
|
||||||
|
? (
|
||||||
|
<DocumentLocked
|
||||||
|
onGetAccess={locked.onGetAccess}
|
||||||
|
isRequesting={locked.isRequesting}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: dataUri == null
|
||||||
? (
|
? (
|
||||||
<div className={slots.stage()}>
|
<div className={slots.stage()}>
|
||||||
<SpinnerGapIcon className={slots.spinner()} />
|
<SpinnerGapIcon className={slots.spinner()} />
|
||||||
|
|||||||
Reference in New Issue
Block a user