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/envelope-signing-provider.tsx b/apps/remix/app/components/general/document-signing/envelope-signing-provider.tsx index aaecd3ee9..eb6e6275f 100644 --- a/apps/remix/app/components/general/document-signing/envelope-signing-provider.tsx +++ b/apps/remix/app/components/general/document-signing/envelope-signing-provider.tsx @@ -13,6 +13,7 @@ import { prop, sortBy } from 'remeda'; import { isBase64Image } from '@documenso/lib/constants/signatures'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import type { EnvelopeForSigningResponse } from '@documenso/lib/server-only/envelope/get-envelope-for-recipient-signing'; +import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import { isFieldUnsignedAndRequired, isRequiredField, @@ -51,7 +52,11 @@ export type EnvelopeSigningContextValue = { setSelectedAssistantRecipientId: (_value: number | null) => 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/envelope-editor/envelope-editor-fields-page-renderer.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx index c75fb52a5..fca9dced8 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 @@ -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. 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..970c60a95 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,7 +113,29 @@ export const EnvelopeEditorFieldsPage = () => { {/* Document View */} -
+
+ {envelope.recipients.length === 0 && ( + +
+ + Missing Recipients + + + You need at least one recipient to add fields + +
+ + +
+ )} + {currentEnvelopeItem !== null ? ( ) : ( @@ -130,7 +153,7 @@ export const EnvelopeEditorFieldsPage = () => {
{/* Right Section - Form Fields Panel */} - {currentEnvelopeItem && ( + {currentEnvelopeItem && envelope.recipients.length > 0 && (
{/* Recipient selector section. */}
@@ -138,29 +161,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 0c505d55c..6f584d46d 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 @@ -323,7 +323,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 fa19bb6a1..36b1dd976 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-signing/envelope-signer-page-renderer.tsx b/apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx index 1c3c324aa..114611402 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 { t, i18n } = useLingui(); const { currentEnvelopeItem } = useCurrentEnvelopeRender(); const { sessionData } = useOptionalSession(); + const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext(); + const { toast } = useToast(); + const { envelopeData, recipient, recipientFields, recipientFieldsRemaining, showPendingFieldTooltip, - signField, + signField: signFieldInternal, email, setEmail, fullName, @@ -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,6 +363,26 @@ export default function EnvelopeSignerPageRenderer() { fieldGroup.on('pointerdown', handleFieldGroupClick); }; + 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. */ 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/universal/field-renderer/render-checkbox-field.ts b/packages/lib/universal/field-renderer/render-checkbox-field.ts index 28a2cefdc..b3e281037 100644 --- a/packages/lib/universal/field-renderer/render-checkbox-field.ts +++ b/packages/lib/universal/field-renderer/render-checkbox-field.ts @@ -62,16 +62,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/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,