From 487f52e194ae00f264d86a04a0b4d228d5e88f27 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 27 Dec 2024 10:30:44 +0200 Subject: [PATCH] feat: enable optional fields (#1470) --- .../src/app/(signing)/sign/[token]/form.tsx | 14 +++-- .../(signing)/sign/[token]/sign-dialog.tsx | 5 +- .../internal/seal-document.handler.ts | 9 ++-- .../document/complete-document-with-token.ts | 3 +- .../lib/server-only/document/seal-document.ts | 5 +- packages/lib/utils/advanced-fields-helpers.ts | 51 +++++++++++++++++++ .../template-flow/add-template-settings.tsx | 2 +- 7 files changed, 76 insertions(+), 13 deletions(-) create mode 100644 packages/lib/utils/advanced-fields-helpers.ts diff --git a/apps/web/src/app/(signing)/sign/[token]/form.tsx b/apps/web/src/app/(signing)/sign/[token]/form.tsx index 17a33dff3..b69280c71 100644 --- a/apps/web/src/app/(signing)/sign/[token]/form.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/form.tsx @@ -11,6 +11,7 @@ import { useForm } from 'react-hook-form'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token'; import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; +import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers'; import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields'; import { type Field, FieldType, type Recipient, RecipientRole } from '@documenso/prisma/client'; import { trpc } from '@documenso/trpc/react'; @@ -57,26 +58,31 @@ export const SigningForm = ({ // Keep the loading state going if successful since the redirect may take some time. const isSubmitting = formState.isSubmitting || formState.isSubmitSuccessful; + const fieldsRequiringValidation = useMemo( + () => fields.filter(isFieldUnsignedAndRequired), + [fields], + ); + const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE); const uninsertedFields = useMemo(() => { - return sortFieldsByPosition(fields.filter((field) => !field.inserted)); + return sortFieldsByPosition(fieldsRequiringValidation.filter((field) => !field.inserted)); }, [fields]); const fieldsValidated = () => { setValidateUninsertedFields(true); - validateFieldsInserted(fields); + validateFieldsInserted(fieldsRequiringValidation); }; const onFormSubmit = async () => { setValidateUninsertedFields(true); + const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation); + if (hasSignatureField && !signatureValid) { return; } - const isFieldsValid = validateFieldsInserted(fields); - if (!isFieldsValid) { return; } diff --git a/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx b/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx index a3a396a04..1bbffa9e6 100644 --- a/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx @@ -1,7 +1,8 @@ -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { Trans } from '@lingui/macro'; +import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers'; import type { Field } from '@documenso/prisma/client'; import { RecipientRole } from '@documenso/prisma/client'; import { Button } from '@documenso/ui/primitives/button'; @@ -36,7 +37,7 @@ export const SignDialog = ({ }: SignDialogProps) => { const [showDialog, setShowDialog] = useState(false); - const isComplete = fields.every((field) => field.inserted); + const isComplete = useMemo(() => !fieldsContainUnsignedRequiredField(fields), [fields]); const handleOpenChange = (open: boolean) => { if (isSubmitting || !isComplete) { diff --git a/packages/lib/jobs/definitions/internal/seal-document.handler.ts b/packages/lib/jobs/definitions/internal/seal-document.handler.ts index 3103e1a25..da0c6c5be 100644 --- a/packages/lib/jobs/definitions/internal/seal-document.handler.ts +++ b/packages/lib/jobs/definitions/internal/seal-document.handler.ts @@ -23,6 +23,7 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs'; import { ZWebhookDocumentSchema } from '../../../types/webhook-payload'; import { getFile } from '../../../universal/upload/get-file'; import { putPdfFile } from '../../../universal/upload/put-file'; +import { fieldsContainUnsignedRequiredField } from '../../../utils/advanced-fields-helpers'; import { createDocumentAuditLogData } from '../../../utils/document-audit-logs'; import type { JobRunIO } from '../../client/_internal/job'; import type { TSealDocumentJobDefinition } from './seal-document'; @@ -105,8 +106,8 @@ export const run = async ({ }, }); - if (fields.some((field) => !field.inserted)) { - throw new Error(`Document ${document.id} has unsigned fields`); + if (fieldsContainUnsignedRequiredField(fields)) { + throw new Error(`Document ${document.id} has unsigned required fields`); } if (isResealing) { @@ -147,7 +148,9 @@ export const run = async ({ } for (const field of fields) { - await insertFieldInPDF(pdfDoc, field); + if (field.inserted) { + await insertFieldInPDF(pdfDoc, field); + } } // Re-flatten the form to handle our checkbox and radio fields that diff --git a/packages/lib/server-only/document/complete-document-with-token.ts b/packages/lib/server-only/document/complete-document-with-token.ts index 3f3338b13..c7cc9491e 100644 --- a/packages/lib/server-only/document/complete-document-with-token.ts +++ b/packages/lib/server-only/document/complete-document-with-token.ts @@ -1,5 +1,6 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; +import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers'; import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs'; import { prisma } from '@documenso/prisma'; import { @@ -85,7 +86,7 @@ export const completeDocumentWithToken = async ({ }, }); - if (fields.some((field) => !field.inserted)) { + if (fieldsContainUnsignedRequiredField(fields)) { throw new Error(`Recipient ${recipient.id} has unsigned fields`); } diff --git a/packages/lib/server-only/document/seal-document.ts b/packages/lib/server-only/document/seal-document.ts index 357bf61e3..b6c3e88fb 100644 --- a/packages/lib/server-only/document/seal-document.ts +++ b/packages/lib/server-only/document/seal-document.ts @@ -18,6 +18,7 @@ import { ZWebhookDocumentSchema } from '../../types/webhook-payload'; import type { RequestMetadata } from '../../universal/extract-request-metadata'; import { getFile } from '../../universal/upload/get-file'; import { putPdfFile } from '../../universal/upload/put-file'; +import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers'; import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf'; import { flattenAnnotations } from '../pdf/flatten-annotations'; import { flattenForm } from '../pdf/flatten-form'; @@ -92,8 +93,8 @@ export const sealDocument = async ({ }, }); - if (fields.some((field) => !field.inserted)) { - throw new Error(`Document ${document.id} has unsigned fields`); + if (fieldsContainUnsignedRequiredField(fields)) { + throw new Error(`Document ${document.id} has unsigned required fields`); } if (isResealing) { diff --git a/packages/lib/utils/advanced-fields-helpers.ts b/packages/lib/utils/advanced-fields-helpers.ts new file mode 100644 index 000000000..9e3978f23 --- /dev/null +++ b/packages/lib/utils/advanced-fields-helpers.ts @@ -0,0 +1,51 @@ +import { type Field, FieldType } from '@documenso/prisma/client'; + +import { ZFieldMetaSchema } from '../types/field-meta'; + +// Currently it seems that the majority of fields have advanced fields for font reasons. +// This array should only contain fields that have an optional setting in the fieldMeta. +const ADVANCED_FIELD_TYPES_WITH_OPTIONAL_SETTING: FieldType[] = [ + FieldType.NUMBER, + FieldType.TEXT, + FieldType.DROPDOWN, + FieldType.RADIO, + FieldType.CHECKBOX, +]; + +/** + * Whether a field is required to be inserted. + */ +export const isRequiredField = (field: Field) => { + // All fields without the optional metadata are assumed to be required. + if (!ADVANCED_FIELD_TYPES_WITH_OPTIONAL_SETTING.includes(field.type)) { + return true; + } + + // Not sure why fieldMeta can be optional for advanced fields, but it is. + // Therefore we must assume if there is no fieldMeta, then the field is optional. + if (!field.fieldMeta) { + return false; + } + + const parsedData = ZFieldMetaSchema.safeParse(field.fieldMeta); + + // If it fails, assume the field is optional. + // This needs to be logged somewhere. + if (!parsedData.success) { + return false; + } + + return parsedData.data?.required === true; +}; + +/** + * Whether the provided field is required and not inserted. + */ +export const isFieldUnsignedAndRequired = (field: Field) => + isRequiredField(field) && !field.inserted; + +/** + * Whether the provided fields contains a field that is required to be inserted. + */ +export const fieldsContainUnsignedRequiredField = (fields: Field[]) => + fields.some(isFieldUnsignedAndRequired); diff --git a/packages/ui/primitives/template-flow/add-template-settings.tsx b/packages/ui/primitives/template-flow/add-template-settings.tsx index 867614b73..241f7b142 100644 --- a/packages/ui/primitives/template-flow/add-template-settings.tsx +++ b/packages/ui/primitives/template-flow/add-template-settings.tsx @@ -245,7 +245,7 @@ export const AddTemplateSettingsFormPartial = ({
  • - Links - We will generate links which you can send to + None - We will generate links which you can send to the recipients manually.