+
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,