From 8663c8f883f34c9ddcdfa1e036939a475f550b90 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 4 Nov 2025 14:57:42 +1100 Subject: [PATCH] fix: various envelope updates --- .../forms/editor/editor-field-text-form.tsx | 24 +++++++ .../document-signing-page-view-v2.tsx | 1 + .../envelope-signing-provider.tsx | 15 +++- .../document/document-certificate-qr-view.tsx | 2 +- .../envelope-editor-fields-page-renderer.tsx | 14 +++- .../envelope-editor-fields-page.tsx | 66 ++++++++++------- .../envelope-editor-settings-dialog.tsx | 3 +- .../envelope-editor-upload-page.tsx | 1 - .../envelope-generic-page-renderer.tsx | 14 +++- .../envelope-signer-page-renderer.tsx | 71 +++++++++++++++---- .../t.$teamUrl+/documents.$id._index.tsx | 5 +- .../t.$teamUrl+/templates.$id._index.tsx | 5 +- .../app/utils/field-signing/checkbox-field.ts | 39 ++++++++-- apps/remix/server/router.ts | 19 +++-- apps/remix/server/trpc/hono-trpc-open-api.ts | 13 +++- .../e2e/api/v2/envelopes-api.spec.ts | 27 +++---- .../include-document-certificate.spec.ts | 2 - .../client-only/hooks/use-editor-fields.ts | 5 +- .../client-only/hooks/use-page-renderer.ts | 6 +- .../providers/envelope-render-provider.tsx | 7 ++ .../lib/server-only/document/send-document.ts | 2 +- .../field-renderer/field-generic-items.ts | 20 ++++++ .../field-renderer/render-checkbox-field.ts | 11 ++- .../universal/field-renderer/render-field.ts | 23 ++++-- ...-field.ts => render-generic-text-field.ts} | 58 ++++++--------- packages/lib/utils/envelope-signing.ts | 4 -- .../set-envelope-fields.types.ts | 1 + .../envelope-router/sign-envelope-field.ts | 43 +++++++++++ .../pdf-viewer/pdf-viewer-konva-lazy.tsx | 3 + .../pdf-viewer/pdf-viewer-konva.tsx | 38 +++++++++- packages/ui/primitives/document-upload.tsx | 2 +- 31 files changed, 400 insertions(+), 144 deletions(-) rename packages/lib/universal/field-renderer/{render-text-field.ts => render-generic-text-field.ts} (77%) diff --git a/apps/remix/app/components/forms/editor/editor-field-text-form.tsx b/apps/remix/app/components/forms/editor/editor-field-text-form.tsx index c634d3a9c..17432944c 100644 --- a/apps/remix/app/components/forms/editor/editor-field-text-form.tsx +++ b/apps/remix/app/components/forms/editor/editor-field-text-form.tsx @@ -152,6 +152,18 @@ export const EditorFieldTextForm = ({ className="h-auto" placeholder={t`Add text to the field`} {...field} + onChange={(e) => { + const values = form.getValues(); + const characterLimit = values.characterLimit || 0; + let textValue = e.target.value; + + if (characterLimit > 0 && textValue.length > characterLimit) { + textValue = textValue.slice(0, characterLimit); + } + + e.target.value = textValue; + field.onChange(e); + }} rows={1} /> @@ -175,6 +187,18 @@ export const EditorFieldTextForm = ({ className="bg-background" placeholder={t`Field character limit`} {...field} + onChange={(e) => { + field.onChange(e); + + const values = form.getValues(); + const characterLimit = parseInt(e.target.value, 10) || 0; + + const textValue = values.text || ''; + + if (characterLimit > 0 && textValue.length > characterLimit) { + form.setValue('text', textValue.slice(0, characterLimit)); + } + }} /> diff --git a/apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx b/apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx index c75aba3da..83d5dc780 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx @@ -205,6 +205,7 @@ export const DocumentSigningPageViewV2 = () => {
{currentEnvelopeItem ? ( void; selectedAssistantRecipient: EnvelopeForSigningResponse['envelope']['recipients'][number] | null; - signField: (_fieldId: number, _value: TSignEnvelopeFieldValue) => Promise; + signField: ( + _fieldId: number, + _value: TSignEnvelopeFieldValue, + authOptions?: TRecipientActionAuth, + ) => Promise; }; const EnvelopeSigningContext = createContext(null); @@ -284,7 +289,11 @@ export const EnvelopeSigningProvider = ({ : null; }, [envelope.documentMeta?.signingOrder, envelope.recipients, recipient.id]); - const signField = async (fieldId: number, fieldValue: TSignEnvelopeFieldValue) => { + const signField = async ( + fieldId: number, + fieldValue: TSignEnvelopeFieldValue, + authOptions?: TRecipientActionAuth, + ) => { // Set the field locally for direct templates. if (isDirectTemplate) { handleDirectTemplateFieldInsertion(fieldId, fieldValue); @@ -295,7 +304,7 @@ export const EnvelopeSigningProvider = ({ token: envelopeData.recipient.token, fieldId, fieldValue, - authOptions: undefined, + authOptions, }); }; diff --git a/apps/remix/app/components/general/document/document-certificate-qr-view.tsx b/apps/remix/app/components/general/document/document-certificate-qr-view.tsx index b4360d6bb..33f3bdf9e 100644 --- a/apps/remix/app/components/general/document/document-certificate-qr-view.tsx +++ b/apps/remix/app/components/general/document/document-certificate-qr-view.tsx @@ -174,7 +174,7 @@ const DocumentCertificateQrV2 = ({
- +
); diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx index c75fb52a5..5c26e5a5d 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx @@ -26,7 +26,7 @@ import { fieldButtonList } from './envelope-editor-fields-drag-drop'; export default function EnvelopeEditorFieldsPageRenderer() { const { t, i18n } = useLingui(); const { envelope, editorFields, getRecipientColorKey } = useCurrentEnvelopeEditor(); - const { currentEnvelopeItem } = useCurrentEnvelopeRender(); + const { currentEnvelopeItem, setRenderError } = useCurrentEnvelopeRender(); const interactiveTransformer = useRef(null); @@ -103,7 +103,6 @@ export default function EnvelopeEditorFieldsPageRenderer() { fieldUpdates.height = fieldPageHeight; } - // Todo: envelopes Use id editorFields.updateFieldByFormId(fieldFormId, fieldUpdates); // Select the field if it is not already selected. @@ -114,7 +113,7 @@ export default function EnvelopeEditorFieldsPageRenderer() { pageLayer.current?.batchDraw(); }; - const renderFieldOnLayer = (field: TLocalField) => { + const unsafeRenderFieldOnLayer = (field: TLocalField) => { if (!pageLayer.current) { return; } @@ -160,6 +159,15 @@ export default function EnvelopeEditorFieldsPageRenderer() { fieldGroup.on('dragend', handleResizeOrMove); }; + const renderFieldOnLayer = (field: TLocalField) => { + try { + unsafeRenderFieldOnLayer(field); + } catch (err) { + console.error(err); + setRenderError(true); + } + }; + /** * Initialize the Konva page canvas and all fields and interactions. */ diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx index 4bd0915da..ceb8e072a 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx @@ -27,7 +27,8 @@ import type { import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients'; import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out'; import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy'; -import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; +import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; +import { Button } from '@documenso/ui/primitives/button'; import { RecipientSelector } from '@documenso/ui/primitives/recipient-selector'; import { Separator } from '@documenso/ui/primitives/separator'; @@ -112,9 +113,34 @@ export const EnvelopeEditorFieldsPage = () => { {/* Document View */} -
+
+ {envelope.recipients.length === 0 && ( + +
+ + Missing Recipients + + + You need at least one recipient to add fields + +
+ + +
+ )} + {currentEnvelopeItem !== null ? ( - + ) : (
@@ -130,7 +156,7 @@ export const EnvelopeEditorFieldsPage = () => {
{/* Right Section - Form Fields Panel */} - {currentEnvelopeItem && ( + {currentEnvelopeItem && envelope.recipients.length > 0 && (
{/* Recipient selector section. */}
@@ -138,29 +164,15 @@ export const EnvelopeEditorFieldsPage = () => { Selected Recipient - {envelope.recipients.length === 0 ? ( - - - You need at least one recipient to add fields - - -

- Click here to add a recipient -

- -
-
- ) : ( - - editorFields.setSelectedRecipient(recipient.id) - } - recipients={envelope.recipients} - className="w-full" - align="end" - /> - )} + + editorFields.setSelectedRecipient(recipient.id) + } + recipients={envelope.recipients} + className="w-full" + align="end" + /> {editorFields.selectedRecipient && !canRecipientFieldsBeModified(editorFields.selectedRecipient, envelope.fields) && ( diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx index aa7baf7da..e30c33c15 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx @@ -229,7 +229,6 @@ export const EnvelopeEditorSettingsDialog = ({ const emails = emailData?.data || []; - // Todo: Envelopes this doesn't make sense (look at previous) const canUpdateVisibility = canAccessTeamDocument(team.currentTeamRole, envelope.visibility); const onFormSubmit = async (data: TAddSettingsFormSchema) => { @@ -322,7 +321,7 @@ export const EnvelopeEditorSettingsDialog = ({ {/* Sidebar. */} -
+
Document Settings diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx index 5b9322a79..5eba88933 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx @@ -203,7 +203,6 @@ export const EnvelopeEditorUploadPage = () => { debouncedUpdateEnvelopeItems(items); }; - // Todo: Envelopes - Sync into envelopes data const debouncedUpdateEnvelopeItems = useDebounceFunction((files: LocalFile[]) => { void updateEnvelopeItems({ envelopeId: envelope.id, diff --git a/apps/remix/app/components/general/envelope-editor/envelope-generic-page-renderer.tsx b/apps/remix/app/components/general/envelope-editor/envelope-generic-page-renderer.tsx index f6ae5c5d7..74635816b 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-generic-page-renderer.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-generic-page-renderer.tsx @@ -12,7 +12,8 @@ import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields'; export default function EnvelopeGenericPageRenderer() { const { i18n } = useLingui(); - const { currentEnvelopeItem, fields, getRecipientColorKey } = useCurrentEnvelopeRender(); + const { currentEnvelopeItem, fields, getRecipientColorKey, setRenderError } = + useCurrentEnvelopeRender(); const { stage, @@ -37,7 +38,7 @@ export default function EnvelopeGenericPageRenderer() { [fields, pageContext.pageNumber], ); - const renderFieldOnLayer = (field: TEnvelope['fields'][number]) => { + const unsafeRenderFieldOnLayer = (field: TEnvelope['fields'][number]) => { if (!pageLayer.current) { console.error('Layer not loaded yet'); return; @@ -66,6 +67,15 @@ export default function EnvelopeGenericPageRenderer() { }); }; + const renderFieldOnLayer = (field: TEnvelope['fields'][number]) => { + try { + unsafeRenderFieldOnLayer(field); + } catch (err) { + console.error(err); + setRenderError(true); + } + }; + /** * Initialize the Konva page canvas and all fields and interactions. */ diff --git a/apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx b/apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx index 1c3c324aa..29a768112 100644 --- a/apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx +++ b/apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx @@ -10,14 +10,17 @@ import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-rende import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider'; import { useOptionalSession } from '@documenso/lib/client-only/providers/session'; import { DIRECT_TEMPLATE_RECIPIENT_EMAIL } from '@documenso/lib/constants/direct-templates'; +import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import { ZFullFieldSchema } from '@documenso/lib/types/field'; import { createSpinner } from '@documenso/lib/universal/field-renderer/field-generic-items'; import { renderField } from '@documenso/lib/universal/field-renderer/render-field'; import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers'; import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields'; import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; +import type { TSignEnvelopeFieldValue } from '@documenso/trpc/server/envelope-router/sign-envelope-field.types'; import { EnvelopeFieldToolTip } from '@documenso/ui/components/field/envelope-field-tooltip'; import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors'; +import { useToast } from '@documenso/ui/primitives/use-toast'; import { handleCheckboxFieldClick } from '~/utils/field-signing/checkbox-field'; import { handleDropdownFieldClick } from '~/utils/field-signing/dropdown-field'; @@ -28,20 +31,24 @@ import { handleNumberFieldClick } from '~/utils/field-signing/number-field'; import { handleSignatureFieldClick } from '~/utils/field-signing/signature-field'; import { handleTextFieldClick } from '~/utils/field-signing/text-field'; +import { useRequiredDocumentSigningAuthContext } from '../document-signing/document-signing-auth-provider'; import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider'; export default function EnvelopeSignerPageRenderer() { - const { i18n } = useLingui(); - const { currentEnvelopeItem } = useCurrentEnvelopeRender(); + const { t, i18n } = useLingui(); + const { currentEnvelopeItem, setRenderError } = useCurrentEnvelopeRender(); const { sessionData } = useOptionalSession(); + const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext(); + const { toast } = useToast(); + const { envelopeData, recipient, recipientFields, recipientFieldsRemaining, showPendingFieldTooltip, - signField, + signField: signFieldInternal, email, setEmail, fullName, @@ -80,7 +87,7 @@ export default function EnvelopeSignerPageRenderer() { ); }, [recipientFields, selectedAssistantRecipientFields, pageContext.pageNumber]); - const renderFieldOnLayer = (unparsedField: Field & { signature?: Signature | null }) => { + const unsafeRenderFieldOnLayer = (unparsedField: Field & { signature?: Signature | null }) => { if (!pageLayer.current) { console.error('Layer not loaded yet'); return; @@ -237,7 +244,7 @@ export default function EnvelopeSignerPageRenderer() { .then(async (payload) => { if (payload) { fieldGroup.add(loadingSpinnerGroup); - await signField(field.id, payload); // Todo: Envelopes - Handle errors + await signField(field.id, payload); } if (payload?.value) { @@ -318,7 +325,6 @@ export default function EnvelopeSignerPageRenderer() { * SIGNATURE FIELD. */ .with({ type: FieldType.SIGNATURE }, (field) => { - // Todo: Envelopes - Reauth handleSignatureFieldClick({ field, signature, @@ -329,11 +335,21 @@ export default function EnvelopeSignerPageRenderer() { .then(async (payload) => { if (payload) { fieldGroup.add(loadingSpinnerGroup); - await signField(field.id, payload); - } - if (payload?.value) { - setSignature(payload.value); + if (payload.value) { + void executeActionAuthProcedure({ + onReauthFormSubmit: async (authOptions) => { + await signField(field.id, payload, authOptions); + + loadingSpinnerGroup.destroy(); + }, + actionTarget: field.type, + }); + + setSignature(payload.value); + } else { + await signField(field.id, payload); + } } }) .finally(() => { @@ -347,13 +363,42 @@ export default function EnvelopeSignerPageRenderer() { fieldGroup.on('pointerdown', handleFieldGroupClick); }; + const renderFieldOnLayer = (unparsedField: Field & { signature?: Signature | null }) => { + try { + unsafeRenderFieldOnLayer(unparsedField); + } catch (err) { + console.error(err); + setRenderError(true); + } + }; + + const signField = async ( + fieldId: number, + payload: TSignEnvelopeFieldValue, + authOptions?: TRecipientActionAuth, + ) => { + try { + await signFieldInternal(fieldId, payload, authOptions); + } catch (err) { + console.error(err); + + toast({ + title: t`Error`, + description: t`An error occurred while signing the field.`, + variant: 'destructive', + }); + + throw err; + } + }; + /** * Initialize the Konva page canvas and all fields and interactions. */ const createPageCanvas = (currentStage: Konva.Stage, currentPageLayer: Konva.Layer) => { // Render the fields. for (const field of localPageFields) { - renderFieldOnLayer(field); // Todo: Envelopes - [CRITICAL] Handle errors which prevent rendering + renderFieldOnLayer(field); } currentPageLayer.batchDraw(); @@ -369,7 +414,7 @@ export default function EnvelopeSignerPageRenderer() { localPageFields.forEach((field) => { console.log('Field changed/inserted, rendering on canvas'); - renderFieldOnLayer(field); // Todo: Envelopes - [CRITICAL] Handle errors which prevent rendering + renderFieldOnLayer(field); }); pageLayer.current.batchDraw(); @@ -387,7 +432,7 @@ export default function EnvelopeSignerPageRenderer() { pageLayer.current.destroyChildren(); localPageFields.forEach((field) => { - renderFieldOnLayer(field); // Todo: Envelopes - [CRITICAL] Handle errors which prevent rendering + renderFieldOnLayer(field); }); pageLayer.current.batchDraw(); diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx index 4e7814a13..8728cde68 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx @@ -156,7 +156,10 @@ export default function DocumentPage({ params }: Route.ComponentProps) { - + 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 337d85d92..97cba576d 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 @@ -179,7 +179,10 @@ export default function TemplatePage({ params }: Route.ComponentProps) { - + diff --git a/apps/remix/app/utils/field-signing/checkbox-field.ts b/apps/remix/app/utils/field-signing/checkbox-field.ts index 84b11a207..c3bfaca20 100644 --- a/apps/remix/app/utils/field-signing/checkbox-field.ts +++ b/apps/remix/app/utils/field-signing/checkbox-field.ts @@ -1,5 +1,6 @@ import { FieldType } from '@prisma/client'; +import { validateCheckboxLength } from '@documenso/lib/advanced-fields-validation/validate-checkbox'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import type { TFieldCheckbox } from '@documenso/lib/types/field'; import { parseCheckboxCustomText } from '@documenso/lib/utils/fields'; @@ -44,6 +45,13 @@ export const handleCheckboxFieldClick = async ( let checkedValues: number[] | null = newValues.filter((v) => v.isChecked).map((v) => v.index); + if (checkedValues.length === 0) { + return { + type: FieldType.CHECKBOX, + value: [], + }; + } + if (validationRule && validationLength) { const checkboxValidationRule = checkboxValidationSigns.find( (sign) => sign.label === validationRule, @@ -55,12 +63,33 @@ export const handleCheckboxFieldClick = async ( }); } - checkedValues = await SignFieldCheckboxDialog.call({ - fieldMeta: field.fieldMeta, - validationRule: checkboxValidationRule.value, + // Custom logic to make it flow better. + // If "at most" OR "exactly" 1 value then just return the new selected value if exists. + if ( + (checkboxValidationRule.value === '=' || checkboxValidationRule.value === '<=') && + validationLength === 1 + ) { + return { + type: FieldType.CHECKBOX, + value: [clickedCheckboxIndex], + }; + } + + const isValid = validateCheckboxLength( + checkedValues.length, + checkboxValidationRule.value, validationLength, - preselectedIndices: currentCheckedIndices, - }); + ); + + // Only render validation dialog if validation is invalid. + if (!isValid) { + checkedValues = await SignFieldCheckboxDialog.call({ + fieldMeta: field.fieldMeta, + validationRule: checkboxValidationRule.value, + validationLength, + preselectedIndices: checkedValues, + }); + } } if (!checkedValues) { diff --git a/apps/remix/server/router.ts b/apps/remix/server/router.ts index 8f3cd6f40..456df6a9b 100644 --- a/apps/remix/server/router.ts +++ b/apps/remix/server/router.ts @@ -92,12 +92,19 @@ app.use('/api/trpc/*', reactRouterTrpcServer); // Unstable API server routes. Order matters for these two. app.get(`${API_V2_URL}/openapi.json`, (c) => c.json(openApiDocument)); app.use(`${API_V2_URL}/*`, cors()); -app.use(`${API_V2_URL}/*`, async (c) => openApiTrpcServerHandler(c)); +app.use(`${API_V2_URL}/*`, async (c) => + openApiTrpcServerHandler(c, { + isBeta: false, + }), +); -// Redirect /api/v2-beta to /api/v2. -app.all('/api/v2-beta/*', (c) => { - const newPath = c.req.path.replace(API_V2_BETA_URL, API_V2_URL); - return c.redirect(newPath, 301); -}); +// Unstable API server routes. Order matters for these two. +app.get(`${API_V2_BETA_URL}/openapi.json`, (c) => c.json(openApiDocument)); +app.use(`${API_V2_BETA_URL}/*`, cors()); +app.use(`${API_V2_BETA_URL}/*`, async (c) => + openApiTrpcServerHandler(c, { + isBeta: true, + }), +); export default app; diff --git a/apps/remix/server/trpc/hono-trpc-open-api.ts b/apps/remix/server/trpc/hono-trpc-open-api.ts index 8d9f28ec8..36f6951f1 100644 --- a/apps/remix/server/trpc/hono-trpc-open-api.ts +++ b/apps/remix/server/trpc/hono-trpc-open-api.ts @@ -1,15 +1,22 @@ import type { Context } from 'hono'; -import { API_V2_URL } from '@documenso/lib/constants/app'; +import { API_V2_BETA_URL, API_V2_URL } from '@documenso/lib/constants/app'; import { AppError, genericErrorCodeToTrpcErrorCodeMap } from '@documenso/lib/errors/app-error'; import { createTrpcContext } from '@documenso/trpc/server/context'; import { appRouter } from '@documenso/trpc/server/router'; import { createOpenApiFetchHandler } from '@documenso/trpc/utils/openapi-fetch-handler'; import { handleTrpcRouterError } from '@documenso/trpc/utils/trpc-error-handler'; -export const openApiTrpcServerHandler = async (c: Context) => { +type OpenApiTrpcServerHandlerOptions = { + isBeta: boolean; +}; + +export const openApiTrpcServerHandler = async ( + c: Context, + { isBeta }: OpenApiTrpcServerHandlerOptions, +) => { return createOpenApiFetchHandler({ - endpoint: API_V2_URL, + endpoint: isBeta ? API_V2_BETA_URL : API_V2_URL, router: appRouter, createContext: async () => createTrpcContext({ c, requestSource: 'apiV2' }), req: c.req.raw, diff --git a/packages/app-tests/e2e/api/v2/envelopes-api.spec.ts b/packages/app-tests/e2e/api/v2/envelopes-api.spec.ts index 53c7a0878..8127bf379 100644 --- a/packages/app-tests/e2e/api/v2/envelopes-api.spec.ts +++ b/packages/app-tests/e2e/api/v2/envelopes-api.spec.ts @@ -68,7 +68,7 @@ test.describe('API V2 Envelopes', () => { const formData = new FormData(); formData.append('payload', JSON.stringify(payload)); - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/create`, { + const res = await request.post(`${baseUrl}/envelope/create`, { headers: { Authorization: `Bearer ${tokenB}` }, multipart: formData, }); @@ -100,7 +100,7 @@ test.describe('API V2 Envelopes', () => { formData.append('files', new File([file.data], file.name, { type: 'application/pdf' })); } - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/create`, { + const res = await request.post(`${baseUrl}/envelope/create`, { headers: { Authorization: `Bearer ${tokenB}` }, multipart: formData, }); @@ -232,14 +232,14 @@ test.describe('API V2 Envelopes', () => { } // Should error since folder is not owned by the user. - const invalidRes = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/create`, { + const invalidRes = await request.post(`${baseUrl}/envelope/create`, { headers: { Authorization: `Bearer ${tokenB}` }, multipart: formData, }); expect(invalidRes.ok()).toBeFalsy(); - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/create`, { + const res = await request.post(`${baseUrl}/envelope/create`, { headers: { Authorization: `Bearer ${tokenA}` }, multipart: formData, }); @@ -378,21 +378,24 @@ test.describe('API V2 Envelopes', () => { new File([alignmentPdf], 'field-font-alignment.pdf', { type: 'application/pdf' }), ); - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/create`, { + const createEnvelopeRequest = await request.post(`${baseUrl}/envelope/create`, { headers: { Authorization: `Bearer ${tokenA}` }, multipart: formData, }); - const response: TCreateEnvelopeResponse = await res.json(); + expect(createEnvelopeRequest.ok()).toBeTruthy(); + expect(createEnvelopeRequest.status()).toBe(200); - const createdEnvelope: TGetEnvelopeResponse = await request - .get(`${baseUrl}/envelope/${response.id}`, { - headers: { Authorization: `Bearer ${tokenA}` }, - }) - .then(async (res) => await res.json()); + const { id: createdEnvelopeId }: TCreateEnvelopeResponse = await createEnvelopeRequest.json(); + + const getEnvelopeRequest = await request.get(`${baseUrl}/envelope/${createdEnvelopeId}`, { + headers: { Authorization: `Bearer ${tokenA}` }, + }); + + const createdEnvelope: TGetEnvelopeResponse = await getEnvelopeRequest.json(); // Might as well testing access control here as well. - const unauthRequest = await request.get(`${baseUrl}/envelope/${response.id}`, { + const unauthRequest = await request.get(`${baseUrl}/envelope/${createdEnvelopeId}`, { headers: { Authorization: `Bearer ${tokenB}` }, }); diff --git a/packages/app-tests/e2e/features/include-document-certificate.spec.ts b/packages/app-tests/e2e/features/include-document-certificate.spec.ts index e1ab6f915..961c6fb3e 100644 --- a/packages/app-tests/e2e/features/include-document-certificate.spec.ts +++ b/packages/app-tests/e2e/features/include-document-certificate.spec.ts @@ -78,7 +78,6 @@ test.describe('Signing Certificate Tests', () => { }, }); - // Todo: Envelopes const firstDocumentData = completedDocument.envelopeItems[0].documentData; const completedDocumentData = await getFile(firstDocumentData); @@ -169,7 +168,6 @@ test.describe('Signing Certificate Tests', () => { }, }); - // Todo: Envelopes const firstDocumentData = completedDocument.envelopeItems[0].documentData; const completedDocumentData = await getFile(firstDocumentData); diff --git a/packages/lib/client-only/hooks/use-editor-fields.ts b/packages/lib/client-only/hooks/use-editor-fields.ts index 68320856e..14b56a882 100644 --- a/packages/lib/client-only/hooks/use-editor-fields.ts +++ b/packages/lib/client-only/hooks/use-editor-fields.ts @@ -165,10 +165,7 @@ export const useEditorFields = ({ const index = localFields.findIndex((field) => field.formId === formId); if (index !== -1) { - update(index, { - ...localFields[index], - id, - }); + form.setValue(`fields.${index}.id`, id); } }; diff --git a/packages/lib/client-only/hooks/use-page-renderer.ts b/packages/lib/client-only/hooks/use-page-renderer.ts index c2f03e6be..6aa6bd3a2 100644 --- a/packages/lib/client-only/hooks/use-page-renderer.ts +++ b/packages/lib/client-only/hooks/use-page-renderer.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import Konva from 'konva'; import type { RenderParameters } from 'pdfjs-dist/types/src/display/api'; @@ -25,6 +25,8 @@ export function usePageRenderer(renderFunction: RenderFunction) { const stage = useRef(null); const pageLayer = useRef(null); + const [renderError, setRenderError] = useState(false); + /** * The raw viewport with no scaling. Basically the actual PDF size. */ @@ -122,5 +124,7 @@ export function usePageRenderer(renderFunction: RenderFunction) { unscaledViewport, scaledViewport, pageContext, + renderError, + setRenderError, }; } diff --git a/packages/lib/client-only/providers/envelope-render-provider.tsx b/packages/lib/client-only/providers/envelope-render-provider.tsx index 367693dac..ae69e1a76 100644 --- a/packages/lib/client-only/providers/envelope-render-provider.tsx +++ b/packages/lib/client-only/providers/envelope-render-provider.tsx @@ -27,6 +27,9 @@ type EnvelopeRenderProviderValue = { setCurrentEnvelopeItem: (envelopeItemId: string) => void; fields: TEnvelope['fields']; getRecipientColorKey: (recipientId: number) => TRecipientColor; + + renderError: boolean; + setRenderError: (renderError: boolean) => void; }; interface EnvelopeRenderProviderProps { @@ -74,6 +77,8 @@ export const EnvelopeRenderProvider = ({ const [currentItem, setItem] = useState(null); + const [renderError, setRenderError] = useState(false); + const envelopeItems = useMemo( () => envelope.envelopeItems.sort((a, b) => a.order - b.order), [envelope.envelopeItems], @@ -164,6 +169,8 @@ export const EnvelopeRenderProvider = ({ setCurrentEnvelopeItem, fields: fields ?? [], getRecipientColorKey, + renderError, + setRenderError, }} > {children} diff --git a/packages/lib/server-only/document/send-document.ts b/packages/lib/server-only/document/send-document.ts index a7e77a27a..3b897da7f 100644 --- a/packages/lib/server-only/document/send-document.ts +++ b/packages/lib/server-only/document/send-document.ts @@ -250,7 +250,7 @@ export const sendDocument = async ({ ); } - if (isValid) { + if (isValid && checkedIndices.length > 0) { fieldsToAutoInsert.push({ fieldId, customText: toCheckboxCustomText(checkedIndices), diff --git a/packages/lib/universal/field-renderer/field-generic-items.ts b/packages/lib/universal/field-renderer/field-generic-items.ts index 9b5d948b1..af196c000 100644 --- a/packages/lib/universal/field-renderer/field-generic-items.ts +++ b/packages/lib/universal/field-renderer/field-generic-items.ts @@ -153,6 +153,11 @@ export const createFieldHoverInteraction = ({ const hoverColor = RECIPIENT_COLOR_STYLES[options.color].baseRingHover; fieldGroup.on('mouseover', () => { + const layer = fieldRect.getLayer(); + if (!layer) { + return; + } + new Konva.Tween({ node: fieldRect, duration: 0.3, @@ -161,6 +166,11 @@ export const createFieldHoverInteraction = ({ }); fieldGroup.on('mouseout', () => { + const layer = fieldRect.getLayer(); + if (!layer) { + return; + } + new Konva.Tween({ node: fieldRect, duration: 0.3, @@ -169,6 +179,11 @@ export const createFieldHoverInteraction = ({ }); fieldGroup.on('transformstart', () => { + const layer = fieldRect.getLayer(); + if (!layer) { + return; + } + new Konva.Tween({ node: fieldRect, duration: 0.3, @@ -177,6 +192,11 @@ export const createFieldHoverInteraction = ({ }); fieldGroup.on('transformend', () => { + const layer = fieldRect.getLayer(); + if (!layer) { + return; + } + new Konva.Tween({ node: fieldRect, duration: 0.3, diff --git a/packages/lib/universal/field-renderer/render-checkbox-field.ts b/packages/lib/universal/field-renderer/render-checkbox-field.ts index bb22bd419..8a927201d 100644 --- a/packages/lib/universal/field-renderer/render-checkbox-field.ts +++ b/packages/lib/universal/field-renderer/render-checkbox-field.ts @@ -63,16 +63,15 @@ export const renderCheckboxFieldElement = ( const rectWidth = fieldRect.width() * groupScaleX; const rectHeight = fieldRect.height() * groupScaleY; - // Todo: Envelopes - check sorting more than 10 - // arr.sort((a, b) => a.localeCompare(b, undefined, { numeric: true })); - const squares = fieldGroup .find('.checkbox-square') - .sort((a, b) => a.id().localeCompare(b.id())); + .sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true })); const checkmarks = fieldGroup .find('.checkbox-checkmark') - .sort((a, b) => a.id().localeCompare(b.id())); - const text = fieldGroup.find('.checkbox-text').sort((a, b) => a.id().localeCompare(b.id())); + .sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true })); + const text = fieldGroup + .find('.checkbox-text') + .sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true })); const groupedItems = squares.map((square, i) => ({ squareElement: square, diff --git a/packages/lib/universal/field-renderer/render-field.ts b/packages/lib/universal/field-renderer/render-field.ts index 6e866b6be..a96d0dd68 100644 --- a/packages/lib/universal/field-renderer/render-field.ts +++ b/packages/lib/universal/field-renderer/render-field.ts @@ -8,9 +8,9 @@ import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors'; import type { TFieldMetaSchema } from '../../types/field-meta'; import { renderCheckboxFieldElement } from './render-checkbox-field'; import { renderDropdownFieldElement } from './render-dropdown-field'; +import { renderGenericTextFieldElement } from './render-generic-text-field'; import { renderRadioFieldElement } from './render-radio-field'; import { renderSignatureFieldElement } from './render-signature-field'; -import { renderTextFieldElement } from './render-text-field'; export const MIN_FIELD_HEIGHT_PX = 12; export const MIN_FIELD_WIDTH_PX = 36; @@ -43,9 +43,9 @@ type RenderFieldOptions = { * * @default 'edit' * - * - `edit` - The field is rendered in edit mode. - * - `sign` - The field is rendered in sign mode. No interactive elements. - * - `export` - The field is rendered in export mode. No backgrounds, interactive elements, etc. + * - `edit` - The field is rendered in editor page. + * - `sign` - The field is rendered for the signing page. + * - `export` - The field is rendered for exporting and sealing into the PDF. No backgrounds, interactive elements, etc. */ mode: 'edit' | 'sign' | 'export'; @@ -76,10 +76,21 @@ export const renderField = ({ }; return match(field.type) - .with(FieldType.TEXT, () => renderTextFieldElement(field, options)) + .with( + FieldType.INITIALS, + FieldType.NAME, + FieldType.EMAIL, + FieldType.DATE, + FieldType.TEXT, + FieldType.NUMBER, + () => renderGenericTextFieldElement(field, options), + ) .with(FieldType.CHECKBOX, () => renderCheckboxFieldElement(field, options)) .with(FieldType.RADIO, () => renderRadioFieldElement(field, options)) .with(FieldType.DROPDOWN, () => renderDropdownFieldElement(field, options)) .with(FieldType.SIGNATURE, () => renderSignatureFieldElement(field, options)) - .otherwise(() => renderTextFieldElement(field, options)); // Todo: Envelopes + .with(FieldType.FREE_SIGNATURE, () => { + throw new Error('Free signature fields are not supported'); + }) + .exhaustive(); }; diff --git a/packages/lib/universal/field-renderer/render-text-field.ts b/packages/lib/universal/field-renderer/render-generic-text-field.ts similarity index 77% rename from packages/lib/universal/field-renderer/render-text-field.ts rename to packages/lib/universal/field-renderer/render-generic-text-field.ts index c1fc95227..e3c3ee83e 100644 --- a/packages/lib/universal/field-renderer/render-text-field.ts +++ b/packages/lib/universal/field-renderer/render-generic-text-field.ts @@ -12,6 +12,8 @@ import { import type { FieldToRender, RenderFieldElementOptions } from './field-renderer'; import { calculateFieldPosition } from './field-renderer'; +const DEFAULT_TEXT_ALIGN = 'left'; + const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOptions): Konva.Text => { const { pageWidth, pageHeight, mode = 'edit', pageLayer, translations } = options; @@ -31,8 +33,8 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption // Calculate text positioning based on alignment const textX = 0; const textY = 0; - let textAlign: 'left' | 'center' | 'right' = textMeta?.textAlign || 'left'; - let textVerticalAlign: 'top' | 'middle' | 'bottom' = 'top'; + let textAlign: 'left' | 'center' | 'right' = textMeta?.textAlign || DEFAULT_TEXT_ALIGN; + const textVerticalAlign: 'top' | 'middle' | 'bottom' = 'middle'; const textFontSize = textMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE; const textPadding = 10; @@ -40,51 +42,33 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption // Handle edit mode. if (mode === 'edit') { - textToRender = fieldTypeName; - textAlign = 'center'; - textVerticalAlign = 'middle'; - - if (textMeta?.label) { - textToRender = textMeta.label; - } else if (textMeta?.text) { + if (textMeta?.text) { textToRender = textMeta.text; - textAlign = textMeta.textAlign || 'center'; // Todo: Envelopes - What is the default - - // Todo: Envelopes - Handle this on signatures - if (textMeta.characterLimit) { - textToRender = textToRender.slice(0, textMeta.characterLimit); - } + } else if (textMeta?.label) { + textToRender = textMeta.label; + } else { + // Show field name which is centered for the edit mode if no label/text is avaliable. + textToRender = fieldTypeName; + textAlign = 'center'; } } // Handle sign mode. if (mode === 'sign' || mode === 'export') { - textToRender = fieldTypeName; - textAlign = 'center'; - textVerticalAlign = 'middle'; - - if (textMeta?.label) { - textToRender = textMeta.label; - } - - if (textMeta?.text) { - textToRender = textMeta.text; - textAlign = textMeta.textAlign || 'center'; // Todo: Envelopes - What is the default - - // Todo: Envelopes - Handle this on signatures - if (textMeta.characterLimit) { - textToRender = textToRender.slice(0, textMeta.characterLimit); + if (!field.inserted) { + if (textMeta?.text) { + textToRender = textMeta.text; + } else if (textMeta?.label) { + textToRender = textMeta.label; + } else if (mode === 'sign') { + // Only show the field name in sign mode if no text/label is avaliable. + textToRender = fieldTypeName; + textAlign = 'center'; } } if (field.inserted) { textToRender = field.customText; - textAlign = textMeta?.textAlign || 'center'; // Todo: Envelopes - What is the default - - // Todo: Envelopes - Handle this on signatures - if (textMeta?.characterLimit) { - textToRender = textToRender.slice(0, textMeta.characterLimit); - } } } @@ -106,7 +90,7 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption return fieldText; }; -export const renderTextFieldElement = ( +export const renderGenericTextFieldElement = ( field: FieldToRender, options: RenderFieldElementOptions, ) => { diff --git a/packages/lib/utils/envelope-signing.ts b/packages/lib/utils/envelope-signing.ts index 8f43773e9..10bc01593 100644 --- a/packages/lib/utils/envelope-signing.ts +++ b/packages/lib/utils/envelope-signing.ts @@ -104,7 +104,6 @@ export const extractFieldInsertionValues = ({ const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta); const errors = validateNumberField(fieldValue.value.toString(), numberFieldParsedMeta, true); - // Todo if (errors.length > 0) { throw new AppError(AppErrorCode.INVALID_BODY, { message: 'Invalid number', @@ -127,7 +126,6 @@ export const extractFieldInsertionValues = ({ const parsedTextFieldMeta = ZTextFieldMeta.parse(field.fieldMeta); const errors = validateTextField(fieldValue.value, parsedTextFieldMeta, true); - // Todo if (errors.length > 0) { throw new AppError(AppErrorCode.INVALID_BODY, { message: 'Invalid email', @@ -189,7 +187,6 @@ export const extractFieldInsertionValues = ({ (sign) => sign.label === validationRule, ); - // Todo: Envelopes - Test this. if (checkboxValidationRule) { const isValid = validateCheckboxLength( selectedValues.length, @@ -224,7 +221,6 @@ export const extractFieldInsertionValues = ({ const parsedDropdownFieldMeta = ZDropdownFieldMeta.parse(field.fieldMeta); const errors = validateDropdownField(fieldValue.value, parsedDropdownFieldMeta, true); - // Todo: Envelopes if (errors.length > 0) { throw new AppError(AppErrorCode.INVALID_BODY, { message: 'Invalid dropdown value', diff --git a/packages/trpc/server/envelope-router/set-envelope-fields.types.ts b/packages/trpc/server/envelope-router/set-envelope-fields.types.ts index 19b10552a..752ca39d4 100644 --- a/packages/trpc/server/envelope-router/set-envelope-fields.types.ts +++ b/packages/trpc/server/envelope-router/set-envelope-fields.types.ts @@ -13,6 +13,7 @@ export const ZSetEnvelopeFieldsRequestSchema = z.object({ envelopeId: z.string(), envelopeType: z.nativeEnum(EnvelopeType), fields: z.array( + // Todo: Envelopes - Use strict schema for types + field meta. z.object({ id: z .number() diff --git a/packages/trpc/server/envelope-router/sign-envelope-field.ts b/packages/trpc/server/envelope-router/sign-envelope-field.ts index 0b37cfc2e..fd9695ff7 100644 --- a/packages/trpc/server/envelope-router/sign-envelope-field.ts +++ b/packages/trpc/server/envelope-router/sign-envelope-field.ts @@ -133,6 +133,49 @@ export const signEnvelopeFieldRoute = procedure const insertionValues = extractFieldInsertionValues({ fieldValue, field, documentMeta }); + // Early return for uninserting fields. + if (!insertionValues.inserted) { + return await prisma.$transaction(async (tx) => { + const updatedField = await tx.field.update({ + where: { + id: field.id, + }, + data: { + customText: '', + inserted: false, + }, + }); + + await tx.signature.deleteMany({ + where: { + fieldId: field.id, + }, + }); + + if (recipient.role !== RecipientRole.ASSISTANT) { + await tx.documentAuditLog.create({ + data: createDocumentAuditLogData({ + type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED, + envelopeId: envelope.id, + user: { + name: recipient.name, + email: recipient.email, + }, + requestMetadata: metadata.requestMetadata, + data: { + field: field.type, + fieldId: field.secondaryId, + }, + }), + }); + } + + return { + signedField: updatedField, + }; + }); + } + const derivedRecipientActionAuth = await validateFieldAuth({ documentAuthOptions: envelope.authOptions, recipient, diff --git a/packages/ui/components/pdf-viewer/pdf-viewer-konva-lazy.tsx b/packages/ui/components/pdf-viewer/pdf-viewer-konva-lazy.tsx index 954bda37e..2d5d38658 100644 --- a/packages/ui/components/pdf-viewer/pdf-viewer-konva-lazy.tsx +++ b/packages/ui/components/pdf-viewer/pdf-viewer-konva-lazy.tsx @@ -2,11 +2,14 @@ import React, { Suspense, lazy } from 'react'; import { type PDFDocumentProxy } from 'pdfjs-dist'; +import type { PdfViewerRendererMode } from './pdf-viewer-konva'; + export type LoadedPDFDocument = PDFDocumentProxy; export type PDFViewerProps = { className?: string; onDocumentLoad?: () => void; + renderer: PdfViewerRendererMode; [key: string]: unknown; } & Omit, 'onPageClick'>; diff --git a/packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx b/packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx index 4dca7ae4c..32e73e6b5 100644 --- a/packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx +++ b/packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx @@ -1,6 +1,8 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Trans } from '@lingui/react/macro'; +import type { MessageDescriptor } from '@lingui/core'; +import { msg } from '@lingui/core/macro'; +import { Trans, useLingui } from '@lingui/react/macro'; import Konva from 'konva'; import { Loader } from 'lucide-react'; import { type PDFDocumentProxy } from 'pdfjs-dist'; @@ -8,6 +10,7 @@ import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf'; import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider'; import { cn } from '@documenso/ui/lib/utils'; +import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; export type LoadedPDFDocument = PDFDocumentProxy; @@ -29,10 +32,31 @@ const PDFLoader = () => ( ); +export type PdfViewerRendererMode = 'editor' | 'preview' | 'signing'; + +const RendererErrorMessages: Record< + PdfViewerRendererMode, + { title: MessageDescriptor; description: MessageDescriptor } +> = { + editor: { + title: msg`Configuration Error`, + description: msg`There was an issue rendering some fields, please review the fields and try again.`, + }, + preview: { + title: msg`Configuration Error`, + description: msg`Something went wrong while rendering the document, some fields may be missing or corrupted.`, + }, + signing: { + title: msg`Configuration Error`, + description: msg`Something went wrong while rendering the document, some fields may be missing or corrupted.`, + }, +}; + export type PdfViewerKonvaProps = { className?: string; onDocumentLoad?: () => void; customPageRenderer?: React.FunctionComponent; + renderer: PdfViewerRendererMode; [key: string]: unknown; } & Omit, 'onPageClick'>; @@ -40,11 +64,14 @@ export const PdfViewerKonva = ({ className, onDocumentLoad, customPageRenderer, + renderer, ...props }: PdfViewerKonvaProps) => { + const { t } = useLingui(); + const $el = useRef(null); - const { getPdfBuffer, currentEnvelopeItem } = useCurrentEnvelopeRender(); + const { getPdfBuffer, currentEnvelopeItem, renderError } = useCurrentEnvelopeRender(); const [width, setWidth] = useState(0); const [numPages, setNumPages] = useState(0); @@ -92,6 +119,13 @@ export const PdfViewerKonva = ({ return (
+ {renderError && ( + + {t(RendererErrorMessages[renderer].title)} + {t(RendererErrorMessages[renderer].description)} + + )} + {envelopeItemFile && Konva ? (