+
{match({ isLoading, document })
.with({ isLoading: true }, () => (
@@ -229,7 +229,7 @@ export const MultiSignDocumentSigningView = ({
'md:mx-auto md:max-w-2xl': document.status === DocumentStatus.COMPLETED,
})}
>
-
)}
+
+ {hasDocumentLoaded && showPendingFieldTooltip && pendingFields.length > 0 && (
+
+
+ Click to insert field
+
+
+ )}
+
+ {/* Fields */}
+ {hasDocumentLoaded && (
+
+ )}
+
+ {/* Completed fields */}
+ {document.status !== DocumentStatus.COMPLETED && (
+
+ )}
-
- {hasDocumentLoaded && showPendingFieldTooltip && pendingFields.length > 0 && (
-
-
- Click to insert field
-
-
- )}
-
- {/* Fields */}
- {hasDocumentLoaded && (
-
- )}
-
- {/* Completed fields */}
- {document.status !== DocumentStatus.COMPLETED && (
-
- )}
>
))
.otherwise(() => null)}
diff --git a/apps/remix/app/components/general/direct-template/direct-template-page.tsx b/apps/remix/app/components/general/direct-template/direct-template-page.tsx
index d14044d45..4dd3038cf 100644
--- a/apps/remix/app/components/general/direct-template/direct-template-page.tsx
+++ b/apps/remix/app/components/general/direct-template/direct-template-page.tsx
@@ -19,7 +19,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredDocumentSigningAuthContext } from '~/components/general/document-signing/document-signing-auth-provider';
import { useRequiredDocumentSigningContext } from '~/components/general/document-signing/document-signing-provider';
-import { PDFViewer } from '~/components/general/pdf-viewer/pdf-viewer';
+import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
import {
DirectTemplateConfigureForm,
@@ -152,7 +152,7 @@ export const DirectTemplatePageView = ({
gradient
>
-
-
-
{
const { envelopeItems } = useCurrentEnvelopeRender();
- const scrollableContainerRef = useRef(null);
-
return (
@@ -216,12 +214,12 @@ const DocumentCertificateQrV2 = ({
/>
-
+
diff --git a/apps/remix/app/components/general/document/document-edit-form.tsx b/apps/remix/app/components/general/document/document-edit-form.tsx
index 21b2e307a..ca7043f53 100644
--- a/apps/remix/app/components/general/document/document-edit-form.tsx
+++ b/apps/remix/app/components/general/document/document-edit-form.tsx
@@ -31,7 +31,7 @@ import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/ty
import { Stepper } from '@documenso/ui/primitives/stepper';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import { PDFViewer } from '~/components/general/pdf-viewer/pdf-viewer';
+import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
import { useCurrentTeam } from '~/providers/team';
export type DocumentEditFormProps = {
@@ -441,7 +441,7 @@ export const DocumentEditForm = ({
gradient
>
- {
return (
-
+
{/* Horizontal envelope item selector */}
-
+
{/* Document View */}
diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-preview-page.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-preview-page.tsx
index 086eaa199..aedcd1ed6 100644
--- a/apps/remix/app/components/general/envelope-editor/envelope-editor-preview-page.tsx
+++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-preview-page.tsx
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useRef, useState } from 'react';
-import { faker } from '@faker-js/faker/locale/en';
+import type { Faker } from '@faker-js/faker';
import { Trans } from '@lingui/react/macro';
import { FieldType, SigningStatus } from '@prisma/client';
import { FileTextIcon } from 'lucide-react';
@@ -26,9 +26,8 @@ import { EnvelopePdfViewer } from '~/components/general/pdf-viewer/envelope-pdf-
import { EnvelopeRendererFileSelector } from './envelope-file-selector';
-// Todo: Envelopes - Dynamically import faker
export const EnvelopeEditorPreviewPage = () => {
- const { envelope, editorFields } = useCurrentEnvelopeEditor();
+ const { envelope, editorFields, editorConfig } = useCurrentEnvelopeEditor();
const { currentEnvelopeItem, fields } = useCurrentEnvelopeRender();
@@ -38,7 +37,20 @@ export const EnvelopeEditorPreviewPage = () => {
'recipient',
);
+ const [fakerInstance, setFakerInstance] = useState
(null);
+
+ useEffect(() => {
+ void import('@faker-js/faker/locale/en').then((mod) => {
+ setFakerInstance(mod.faker);
+ });
+ }, []);
+
const fieldsWithPlaceholders = useMemo(() => {
+ if (!fakerInstance) {
+ return [];
+ }
+
+ const faker = fakerInstance;
return fields.map((field) => {
const fieldMeta = ZFieldAndMetaSchema.parse(field);
@@ -189,7 +201,7 @@ export const EnvelopeEditorPreviewPage = () => {
.exhaustive(),
};
});
- }, [fields, envelope, envelope.recipients, envelope.documentMeta]);
+ }, [fields, envelope, envelope.recipients, envelope.documentMeta, fakerInstance]);
/**
* Set the selected recipient to the first recipient in the envelope.
@@ -210,26 +222,30 @@ export const EnvelopeEditorPreviewPage = () => {
...recipient,
signingStatus: SigningStatus.SIGNED,
}))}
+ presignToken={editorConfig?.embedded?.presignToken}
overrideSettings={{
mode: 'export',
}}
>
-
+
{/* Horizontal envelope item selector */}
-
+
+
+
+
+ Preview Mode
+
+
+ Preview what the signed document will look like with placeholder data
+
+
{/* Document View */}
-
-
- Preview Mode
-
-
- Preview what the signed document will look like with placeholder data
-
-
-
{currentEnvelopeItem !== null ? (
+
{envelopeItems.map((doc, i) => (
import('./pdf-viewer'));
+
+export default function PDFViewerLazy(props: PDFViewerProps) {
+ const [isClient, setIsClient] = useState(false);
+
+ useEffect(() => {
+ setIsClient(true);
+ }, []);
+
+ const fallback = (
+
+ );
+
+ if (!isClient) {
+ return fallback;
+ }
+
+ return {PDFViewer && };
+}
diff --git a/apps/remix/app/components/general/pdf-viewer/pdf-viewer-page-image.tsx b/apps/remix/app/components/general/pdf-viewer/pdf-viewer-page-image.tsx
index 271cedd2c..a050ceaba 100644
--- a/apps/remix/app/components/general/pdf-viewer/pdf-viewer-page-image.tsx
+++ b/apps/remix/app/components/general/pdf-viewer/pdf-viewer-page-image.tsx
@@ -28,7 +28,14 @@ export const PdfViewerPageImage = ({ imageLoadingState, imageProps }: PdfViewerP
)}
{/* The PDF image. */}
- {imageProps.src &&
}
+ {imageProps.src && (
+
+ )}
>
);
};
diff --git a/apps/remix/app/components/general/pdf-viewer/pdf-viewer.tsx b/apps/remix/app/components/general/pdf-viewer/pdf-viewer.tsx
index ac83eb449..487ee5e52 100644
--- a/apps/remix/app/components/general/pdf-viewer/pdf-viewer.tsx
+++ b/apps/remix/app/components/general/pdf-viewer/pdf-viewer.tsx
@@ -62,14 +62,14 @@ export type PDFViewerProps = {
customPageRenderer?: React.FunctionComponent<{ pageData: PageRenderData }>;
} & React.HTMLAttributes;
-export const PDFViewer = ({
+export default function PDFViewer({
className,
data,
scrollParentRef,
onDocumentLoad,
customPageRenderer,
...props
-}: PDFViewerProps) => {
+}: PDFViewerProps) {
const { t } = useLingui();
const { toast } = useToast();
@@ -77,7 +77,7 @@ export const PDFViewer = ({
const [loadingState, setLoadingState] = useState('loading');
- const [pdf, setPdf] = useState(null);
+ const pdfRef = useRef(null);
const [pages, setPages] = useState([]);
@@ -86,11 +86,17 @@ export const PDFViewer = ({
return;
}
+ let isCancelled = false;
+
const fetchMetadata = async () => {
try {
setLoadingState('loading');
setPages([]);
+ if (isCancelled) {
+ return;
+ }
+
let result: Uint8Array | null = typeof data === 'string' ? null : new Uint8Array(data);
if (typeof data === 'string') {
@@ -103,13 +109,24 @@ export const PDFViewer = ({
result = new Uint8Array(await response.arrayBuffer());
}
- const loadedPdf = await pdfjsLib.getDocument({ data: result! }).promise;
-
- if (pdf) {
- await pdf.destroy();
+ if (isCancelled) {
+ return;
}
- setPdf(loadedPdf);
+ const loadedPdf = await pdfjsLib.getDocument({ data: result! }).promise;
+
+ if (isCancelled) {
+ await loadedPdf.destroy();
+ return;
+ }
+
+ // Destroy previous PDF if it exists
+ if (pdfRef.current) {
+ await pdfRef.current.destroy();
+ }
+
+ // eslint-disable-next-line require-atomic-updates
+ pdfRef.current = loadedPdf;
// Fetch the pages
const pages = await pMap(
@@ -125,10 +142,18 @@ export const PDFViewer = ({
},
);
+ if (isCancelled) {
+ return;
+ }
+
setPages(pages);
setLoadingState('loaded');
} catch (err) {
+ if (isCancelled) {
+ return;
+ }
+
console.error(err);
setLoadingState('error');
@@ -143,8 +168,11 @@ export const PDFViewer = ({
void fetchMetadata();
return () => {
- if (pdf) {
- void pdf.destroy();
+ isCancelled = true;
+
+ if (pdfRef.current) {
+ void pdfRef.current.destroy();
+ pdfRef.current = null;
}
};
}, [data]);
@@ -178,19 +206,19 @@ export const PDFViewer = ({
{hasError && }
{/* Loaded State */}
- {loadingState === 'loaded' && pages.length > 0 && pdf && (
+ {loadingState === 'loaded' && pages.length > 0 && pdfRef.current && (
)}
);
-};
+}
type VirtualizedPageListProps = {
scrollParentRef: ScrollTarget;
diff --git a/apps/remix/app/components/general/template/template-edit-form.tsx b/apps/remix/app/components/general/template/template-edit-form.tsx
index 201b780bc..65be073a6 100644
--- a/apps/remix/app/components/general/template/template-edit-form.tsx
+++ b/apps/remix/app/components/general/template/template-edit-form.tsx
@@ -28,7 +28,7 @@ import { AddTemplateSettingsFormPartial } from '@documenso/ui/primitives/templat
import type { TAddTemplateSettingsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-settings.types';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import { PDFViewer } from '~/components/general/pdf-viewer/pdf-viewer';
+import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
import { useCurrentTeam } from '~/providers/team';
export type TemplateEditFormProps = {
@@ -313,7 +313,7 @@ export const TemplateEditForm = ({
gradient
>
-
)}
- {
+ return currentParams.id !== nextParams.id;
+};
+
/**
* This file is very similar for templates as well. Any changes here should also be adjusted there as well.
*
diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
index 23400f21e..25ad62615 100644
--- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
+++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
@@ -24,7 +24,7 @@ import { EnvelopeRendererFileSelector } from '~/components/general/envelope-edit
import { EnvelopeGenericPageRenderer } from '~/components/general/envelope-editor/envelope-generic-page-renderer';
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
import { EnvelopePdfViewer } from '~/components/general/pdf-viewer/envelope-pdf-viewer';
-import { PDFViewer } from '~/components/general/pdf-viewer/pdf-viewer';
+import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
import { TemplateDirectLinkBadge } from '~/components/general/template/template-direct-link-badge';
import { TemplatePageViewDocumentsTable } from '~/components/general/template/template-page-view-documents-table';
import { TemplatePageViewInformation } from '~/components/general/template/template-page-view-information';
@@ -210,7 +210,7 @@ export default function TemplatePage({ params }: Route.ComponentProps) {
documentMeta={mockedDocumentMeta}
/>
- {
+ return currentParams.id !== nextParams.id;
+};
+
/**
* This file is very similar for documents as well. Any changes here should also be adjusted there as well.
*
diff --git a/apps/remix/app/routes/embed+/v2+/authoring+/_layout.tsx b/apps/remix/app/routes/embed+/v2+/authoring+/_layout.tsx
index 9e155e49c..d4ed05047 100644
--- a/apps/remix/app/routes/embed+/v2+/authoring+/_layout.tsx
+++ b/apps/remix/app/routes/embed+/v2+/authoring+/_layout.tsx
@@ -20,6 +20,8 @@ import { injectCss } from '~/utils/css-vars';
import type { Route } from './+types/_layout';
+export const shouldRevalidate = () => false;
+
export const loader = async ({ request }: Route.LoaderArgs) => {
const url = new URL(request.url);
diff --git a/apps/remix/app/routes/embed+/v2+/authoring+/envelope.create._index.tsx b/apps/remix/app/routes/embed+/v2+/authoring+/envelope.create._index.tsx
index 71317c47d..ab3665545 100644
--- a/apps/remix/app/routes/embed+/v2+/authoring+/envelope.create._index.tsx
+++ b/apps/remix/app/routes/embed+/v2+/authoring+/envelope.create._index.tsx
@@ -38,6 +38,8 @@ import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
import type { Route } from './+types/envelope.create._index';
+export const shouldRevalidate = () => false;
+
export const loader = async ({ request }: Route.LoaderArgs) => {
const url = new URL(request.url);
diff --git a/apps/remix/app/routes/embed+/v2+/authoring+/envelope.edit.$id.tsx b/apps/remix/app/routes/embed+/v2+/authoring+/envelope.edit.$id.tsx
index d96294d69..0a6b758a7 100644
--- a/apps/remix/app/routes/embed+/v2+/authoring+/envelope.edit.$id.tsx
+++ b/apps/remix/app/routes/embed+/v2+/authoring+/envelope.edit.$id.tsx
@@ -3,7 +3,7 @@ import { useLayoutEffect, useMemo, useState } from 'react';
import { Trans, useLingui } from '@lingui/react/macro';
import { EnvelopeType } from '@prisma/client';
import { CheckCircle2Icon } from 'lucide-react';
-import { redirect } from 'react-router';
+import { type ShouldRevalidateFunctionArgs, redirect } from 'react-router';
import { EnvelopeEditorProvider } from '@documenso/lib/client-only/providers/envelope-editor-provider';
import type { SupportedLanguageCodes } from '@documenso/lib/constants/i18n';
@@ -30,6 +30,10 @@ import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
import type { Route } from './+types/envelope.edit.$id';
+export const shouldRevalidate = ({ currentParams, nextParams }: ShouldRevalidateFunctionArgs) => {
+ return currentParams.id !== nextParams.id;
+};
+
export const loader = async ({ request, params }: Route.LoaderArgs) => {
const url = new URL(request.url);
diff --git a/apps/remix/server/api/files/routes/get-envelope-item-pdf.ts b/apps/remix/server/api/files/routes/get-envelope-item-pdf.ts
index 587be164e..7f1b25aef 100644
--- a/apps/remix/server/api/files/routes/get-envelope-item-pdf.ts
+++ b/apps/remix/server/api/files/routes/get-envelope-item-pdf.ts
@@ -7,6 +7,7 @@ import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import type { DocumentDataVersion } from '@documenso/lib/types/document';
+import { sha256 } from '@documenso/lib/universal/crypto';
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
import { prisma } from '@documenso/prisma';
@@ -122,6 +123,12 @@ export const handleEnvelopeItemPdfRequest = async ({
const documentDataToUse =
version === 'current' ? envelopeItem.documentData.data : envelopeItem.documentData.initialData;
+ const etag = Buffer.from(sha256(documentDataToUse)).toString('hex');
+
+ if (c.req.header('If-None-Match') === etag) {
+ return c.status(304);
+ }
+
const file = await getFileServerSide({
type: envelopeItem.documentData.type,
data: documentDataToUse,
@@ -137,6 +144,7 @@ export const handleEnvelopeItemPdfRequest = async ({
// Note: Only set these headers on success.
c.header('Content-Type', 'application/pdf');
+ c.header('ETag', etag);
c.header('Cache-Control', `${cacheStrategy}, max-age=31536000, immutable`);
return c.body(file);
diff --git a/apps/remix/vite.config.ts b/apps/remix/vite.config.ts
index 65dd1ec50..76aebc780 100644
--- a/apps/remix/vite.config.ts
+++ b/apps/remix/vite.config.ts
@@ -58,6 +58,7 @@ export default defineConfig({
'playwright',
'playwright-core',
'@playwright/browser-chromium',
+ 'pdfjs-dist',
],
},
optimizeDeps: {
diff --git a/packages/app-tests/e2e/api/v1/template-field-prefill.spec.ts b/packages/app-tests/e2e/api/v1/template-field-prefill.spec.ts
index 15bd2a43e..d2a7f106b 100644
--- a/packages/app-tests/e2e/api/v1/template-field-prefill.spec.ts
+++ b/packages/app-tests/e2e/api/v1/template-field-prefill.spec.ts
@@ -197,7 +197,7 @@ test.describe('Template Field Prefill API v1', () => {
// 7. Navigate to the template
await page.goto(
- `${WEBAPP_BASE_URL}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`,
+ `${WEBAPP_BASE_URL}/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`,
);
// 8. Create a document from the template with prefilled fields
@@ -488,7 +488,7 @@ test.describe('Template Field Prefill API v1', () => {
// 7. Navigate to the template
await page.goto(
- `${WEBAPP_BASE_URL}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`,
+ `${WEBAPP_BASE_URL}/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`,
);
// 8. Create a document from the template without prefilled fields
diff --git a/packages/app-tests/e2e/api/v2/template-field-prefill.spec.ts b/packages/app-tests/e2e/api/v2/template-field-prefill.spec.ts
index 97525de6c..80bddbccc 100644
--- a/packages/app-tests/e2e/api/v2/template-field-prefill.spec.ts
+++ b/packages/app-tests/e2e/api/v2/template-field-prefill.spec.ts
@@ -197,7 +197,7 @@ test.describe('Template Field Prefill API v2', () => {
// 7. Navigate to the template
await page.goto(
- `${WEBAPP_BASE_URL}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`,
+ `${WEBAPP_BASE_URL}/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`,
);
// 8. Create a document from the template with prefilled fields using v2 API
@@ -485,7 +485,7 @@ test.describe('Template Field Prefill API v2', () => {
// 7. Navigate to the template
await page.goto(
- `${WEBAPP_BASE_URL}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`,
+ `${WEBAPP_BASE_URL}/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`,
);
// 8. Create a document from the template without prefilled fields using v2 API
diff --git a/packages/ui/components/field/envelope-field-tooltip.tsx b/packages/ui/components/field/envelope-field-tooltip.tsx
index 25dde4ad4..0cf834ecc 100644
--- a/packages/ui/components/field/envelope-field-tooltip.tsx
+++ b/packages/ui/components/field/envelope-field-tooltip.tsx
@@ -124,7 +124,10 @@ export function EnvelopeFieldToolTip({
-
+
{children}
diff --git a/packages/ui/components/field/field-tooltip.tsx b/packages/ui/components/field/field-tooltip.tsx
index 027978292..3fceb0589 100644
--- a/packages/ui/components/field/field-tooltip.tsx
+++ b/packages/ui/components/field/field-tooltip.tsx
@@ -65,7 +65,7 @@ export function FieldToolTip({ children, color, className = '', field }: FieldTo
diff --git a/packages/ui/styles/theme.css b/packages/ui/styles/theme.css
index 7a5a8b8e6..2f05479ba 100644
--- a/packages/ui/styles/theme.css
+++ b/packages/ui/styles/theme.css
@@ -234,6 +234,15 @@
scrollbar-gutter: stable;
}
+.scrollbar-hidden {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
+
+.scrollbar-hidden::-webkit-scrollbar {
+ display: none;
+}
+
/* .custom-scrollbar::-webkit-scrollbar-track {
border-radius: 10px;
}