mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
feat: implement auto-save functionality for signers in document edit form (#1792)
This commit is contained in:
@ -159,34 +159,37 @@ export const DocumentEditForm = ({
|
||||
return initialStep;
|
||||
});
|
||||
|
||||
const saveSettingsData = async (data: TAddSettingsFormSchema) => {
|
||||
const { timezone, dateFormat, redirectUrl, language, signatureTypes } = data.meta;
|
||||
|
||||
const parsedGlobalAccessAuth = z
|
||||
.array(ZDocumentAccessAuthTypesSchema)
|
||||
.safeParse(data.globalAccessAuth);
|
||||
|
||||
return updateDocument({
|
||||
documentId: document.id,
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId || null,
|
||||
visibility: data.visibility,
|
||||
globalAccessAuth: parsedGlobalAccessAuth.success ? parsedGlobalAccessAuth.data : [],
|
||||
globalActionAuth: data.globalActionAuth ?? [],
|
||||
},
|
||||
meta: {
|
||||
timezone,
|
||||
dateFormat,
|
||||
redirectUrl,
|
||||
language: isValidLanguageCode(language) ? language : undefined,
|
||||
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
|
||||
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
||||
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onAddSettingsFormSubmit = async (data: TAddSettingsFormSchema) => {
|
||||
try {
|
||||
const { timezone, dateFormat, redirectUrl, language, signatureTypes } = data.meta;
|
||||
|
||||
const parsedGlobalAccessAuth = z
|
||||
.array(ZDocumentAccessAuthTypesSchema)
|
||||
.safeParse(data.globalAccessAuth);
|
||||
|
||||
await updateDocument({
|
||||
documentId: document.id,
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId || null,
|
||||
visibility: data.visibility,
|
||||
globalAccessAuth: parsedGlobalAccessAuth.success ? parsedGlobalAccessAuth.data : [],
|
||||
globalActionAuth: data.globalActionAuth ?? [],
|
||||
},
|
||||
meta: {
|
||||
timezone,
|
||||
dateFormat,
|
||||
redirectUrl,
|
||||
language: isValidLanguageCode(language) ? language : undefined,
|
||||
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
|
||||
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
||||
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||
},
|
||||
});
|
||||
|
||||
await saveSettingsData(data);
|
||||
setStep('signers');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@ -199,26 +202,58 @@ export const DocumentEditForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onAddSettingsFormAutoSave = async (data: TAddSettingsFormSchema) => {
|
||||
try {
|
||||
await saveSettingsData(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while auto-saving the document settings.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const saveSignersData = async (data: TAddSignersFormSchema) => {
|
||||
return Promise.all([
|
||||
updateDocument({
|
||||
documentId: document.id,
|
||||
meta: {
|
||||
allowDictateNextSigner: data.allowDictateNextSigner,
|
||||
signingOrder: data.signingOrder,
|
||||
},
|
||||
}),
|
||||
|
||||
setRecipients({
|
||||
documentId: document.id,
|
||||
recipients: data.signers.map((signer) => ({
|
||||
...signer,
|
||||
// Explicitly set to null to indicate we want to remove auth if required.
|
||||
actionAuth: signer.actionAuth ?? [],
|
||||
})),
|
||||
}),
|
||||
]);
|
||||
};
|
||||
|
||||
const onAddSignersFormAutoSave = async (data: TAddSignersFormSchema) => {
|
||||
try {
|
||||
await saveSignersData(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while adding signers.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => {
|
||||
try {
|
||||
await Promise.all([
|
||||
updateDocument({
|
||||
documentId: document.id,
|
||||
meta: {
|
||||
allowDictateNextSigner: data.allowDictateNextSigner,
|
||||
signingOrder: data.signingOrder,
|
||||
},
|
||||
}),
|
||||
|
||||
setRecipients({
|
||||
documentId: document.id,
|
||||
recipients: data.signers.map((signer) => ({
|
||||
...signer,
|
||||
// Explicitly set to null to indicate we want to remove auth if required.
|
||||
actionAuth: signer.actionAuth ?? [],
|
||||
})),
|
||||
}),
|
||||
]);
|
||||
await saveSignersData(data);
|
||||
|
||||
setStep('fields');
|
||||
} catch (err) {
|
||||
@ -232,12 +267,16 @@ export const DocumentEditForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const saveFieldsData = async (data: TAddFieldsFormSchema) => {
|
||||
return addFields({
|
||||
documentId: document.id,
|
||||
fields: data.fields,
|
||||
});
|
||||
};
|
||||
|
||||
const onAddFieldsFormSubmit = async (data: TAddFieldsFormSchema) => {
|
||||
try {
|
||||
await addFields({
|
||||
documentId: document.id,
|
||||
fields: data.fields,
|
||||
});
|
||||
await saveFieldsData(data);
|
||||
|
||||
// Clear all field data from localStorage
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
@ -259,24 +298,60 @@ export const DocumentEditForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => {
|
||||
const onAddFieldsFormAutoSave = async (data: TAddFieldsFormSchema) => {
|
||||
try {
|
||||
await saveFieldsData(data);
|
||||
// Don't clear localStorage on auto-save, only on explicit submit
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while auto-saving the fields.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const saveSubjectData = async (data: TAddSubjectFormSchema) => {
|
||||
const { subject, message, distributionMethod, emailId, emailReplyTo, emailSettings } =
|
||||
data.meta;
|
||||
|
||||
try {
|
||||
await sendDocument({
|
||||
documentId: document.id,
|
||||
meta: {
|
||||
subject,
|
||||
message,
|
||||
distributionMethod,
|
||||
emailId,
|
||||
emailReplyTo: emailReplyTo || null,
|
||||
emailSettings: emailSettings,
|
||||
},
|
||||
});
|
||||
return updateDocument({
|
||||
documentId: document.id,
|
||||
meta: {
|
||||
subject,
|
||||
message,
|
||||
distributionMethod,
|
||||
emailId,
|
||||
emailReplyTo,
|
||||
emailSettings: emailSettings,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (distributionMethod === DocumentDistributionMethod.EMAIL) {
|
||||
const sendDocumentWithSubject = async (data: TAddSubjectFormSchema) => {
|
||||
const { subject, message, distributionMethod, emailId, emailReplyTo, emailSettings } =
|
||||
data.meta;
|
||||
|
||||
return sendDocument({
|
||||
documentId: document.id,
|
||||
meta: {
|
||||
subject,
|
||||
message,
|
||||
distributionMethod,
|
||||
emailId,
|
||||
emailReplyTo: emailReplyTo || null,
|
||||
emailSettings,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => {
|
||||
try {
|
||||
await sendDocumentWithSubject(data);
|
||||
|
||||
if (data.meta.distributionMethod === DocumentDistributionMethod.EMAIL) {
|
||||
toast({
|
||||
title: _(msg`Document sent`),
|
||||
description: _(msg`Your document has been sent successfully.`),
|
||||
@ -304,6 +379,21 @@ export const DocumentEditForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onAddSubjectFormAutoSave = async (data: TAddSubjectFormSchema) => {
|
||||
try {
|
||||
// Save form data without sending the document
|
||||
await saveSubjectData(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while auto-saving the subject form.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const currentDocumentFlow = documentFlow[step];
|
||||
|
||||
/**
|
||||
@ -349,25 +439,28 @@ export const DocumentEditForm = ({
|
||||
fields={fields}
|
||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
||||
onSubmit={onAddSettingsFormSubmit}
|
||||
onAutoSave={onAddSettingsFormAutoSave}
|
||||
/>
|
||||
|
||||
<AddSignersFormPartial
|
||||
key={recipients.length}
|
||||
key={document.id}
|
||||
documentFlow={documentFlow.signers}
|
||||
recipients={recipients}
|
||||
signingOrder={document.documentMeta?.signingOrder}
|
||||
allowDictateNextSigner={document.documentMeta?.allowDictateNextSigner}
|
||||
fields={fields}
|
||||
onSubmit={onAddSignersFormSubmit}
|
||||
onAutoSave={onAddSignersFormAutoSave}
|
||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
||||
/>
|
||||
|
||||
<AddFieldsFormPartial
|
||||
key={fields.length}
|
||||
key={document.id}
|
||||
documentFlow={documentFlow.fields}
|
||||
recipients={recipients}
|
||||
fields={fields}
|
||||
onSubmit={onAddFieldsFormSubmit}
|
||||
onAutoSave={onAddFieldsFormAutoSave}
|
||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
||||
teamId={team.id}
|
||||
/>
|
||||
@ -379,6 +472,7 @@ export const DocumentEditForm = ({
|
||||
recipients={recipients}
|
||||
fields={fields}
|
||||
onSubmit={onAddSubjectFormSubmit}
|
||||
onAutoSave={onAddSubjectFormAutoSave}
|
||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
||||
/>
|
||||
</Stepper>
|
||||
|
||||
@ -124,32 +124,36 @@ export const TemplateEditForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const onAddSettingsFormSubmit = async (data: TAddTemplateSettingsFormSchema) => {
|
||||
const saveSettingsData = async (data: TAddTemplateSettingsFormSchema) => {
|
||||
const { signatureTypes } = data.meta;
|
||||
|
||||
const parsedGlobalAccessAuth = z
|
||||
.array(ZDocumentAccessAuthTypesSchema)
|
||||
.safeParse(data.globalAccessAuth);
|
||||
|
||||
return updateTemplateSettings({
|
||||
templateId: template.id,
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId || null,
|
||||
visibility: data.visibility,
|
||||
globalAccessAuth: parsedGlobalAccessAuth.success ? parsedGlobalAccessAuth.data : [],
|
||||
globalActionAuth: data.globalActionAuth ?? [],
|
||||
},
|
||||
meta: {
|
||||
...data.meta,
|
||||
emailReplyTo: data.meta.emailReplyTo || null,
|
||||
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
|
||||
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
||||
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||
language: isValidLanguageCode(data.meta.language) ? data.meta.language : undefined,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onAddSettingsFormSubmit = async (data: TAddTemplateSettingsFormSchema) => {
|
||||
try {
|
||||
await updateTemplateSettings({
|
||||
templateId: template.id,
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId || null,
|
||||
visibility: data.visibility,
|
||||
globalAccessAuth: parsedGlobalAccessAuth.success ? parsedGlobalAccessAuth.data : [],
|
||||
globalActionAuth: data.globalActionAuth ?? [],
|
||||
},
|
||||
meta: {
|
||||
...data.meta,
|
||||
emailReplyTo: data.meta.emailReplyTo || null,
|
||||
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
|
||||
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
||||
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||
language: isValidLanguageCode(data.meta.language) ? data.meta.language : undefined,
|
||||
},
|
||||
});
|
||||
await saveSettingsData(data);
|
||||
|
||||
setStep('signers');
|
||||
} catch (err) {
|
||||
@ -163,24 +167,42 @@ export const TemplateEditForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onAddSettingsFormAutoSave = async (data: TAddTemplateSettingsFormSchema) => {
|
||||
try {
|
||||
await saveSettingsData(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while auto-saving the template settings.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const saveTemplatePlaceholderData = async (data: TAddTemplatePlacholderRecipientsFormSchema) => {
|
||||
return Promise.all([
|
||||
updateTemplateSettings({
|
||||
templateId: template.id,
|
||||
meta: {
|
||||
signingOrder: data.signingOrder,
|
||||
allowDictateNextSigner: data.allowDictateNextSigner,
|
||||
},
|
||||
}),
|
||||
|
||||
setRecipients({
|
||||
templateId: template.id,
|
||||
recipients: data.signers,
|
||||
}),
|
||||
]);
|
||||
};
|
||||
|
||||
const onAddTemplatePlaceholderFormSubmit = async (
|
||||
data: TAddTemplatePlacholderRecipientsFormSchema,
|
||||
) => {
|
||||
try {
|
||||
await Promise.all([
|
||||
updateTemplateSettings({
|
||||
templateId: template.id,
|
||||
meta: {
|
||||
signingOrder: data.signingOrder,
|
||||
allowDictateNextSigner: data.allowDictateNextSigner,
|
||||
},
|
||||
}),
|
||||
|
||||
setRecipients({
|
||||
templateId: template.id,
|
||||
recipients: data.signers,
|
||||
}),
|
||||
]);
|
||||
await saveTemplatePlaceholderData(data);
|
||||
|
||||
setStep('fields');
|
||||
} catch (err) {
|
||||
@ -192,12 +214,46 @@ export const TemplateEditForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onAddTemplatePlaceholderFormAutoSave = async (
|
||||
data: TAddTemplatePlacholderRecipientsFormSchema,
|
||||
) => {
|
||||
try {
|
||||
await saveTemplatePlaceholderData(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while auto-saving the template placeholders.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const saveFieldsData = async (data: TAddTemplateFieldsFormSchema) => {
|
||||
return addTemplateFields({
|
||||
templateId: template.id,
|
||||
fields: data.fields,
|
||||
});
|
||||
};
|
||||
|
||||
const onAddFieldsFormAutoSave = async (data: TAddTemplateFieldsFormSchema) => {
|
||||
try {
|
||||
await saveFieldsData(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while auto-saving the template fields.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onAddFieldsFormSubmit = async (data: TAddTemplateFieldsFormSchema) => {
|
||||
try {
|
||||
await addTemplateFields({
|
||||
templateId: template.id,
|
||||
fields: data.fields,
|
||||
});
|
||||
await saveFieldsData(data);
|
||||
|
||||
// Clear all field data from localStorage
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
@ -270,11 +326,12 @@ export const TemplateEditForm = ({
|
||||
recipients={recipients}
|
||||
fields={fields}
|
||||
onSubmit={onAddSettingsFormSubmit}
|
||||
onAutoSave={onAddSettingsFormAutoSave}
|
||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
||||
/>
|
||||
|
||||
<AddTemplatePlaceholderRecipientsFormPartial
|
||||
key={recipients.length}
|
||||
key={template.id}
|
||||
documentFlow={documentFlow.signers}
|
||||
recipients={recipients}
|
||||
fields={fields}
|
||||
@ -282,15 +339,17 @@ export const TemplateEditForm = ({
|
||||
allowDictateNextSigner={template.templateMeta?.allowDictateNextSigner}
|
||||
templateDirectLink={template.directLink}
|
||||
onSubmit={onAddTemplatePlaceholderFormSubmit}
|
||||
onAutoSave={onAddTemplatePlaceholderFormAutoSave}
|
||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
||||
/>
|
||||
|
||||
<AddTemplateFieldsFormPartial
|
||||
key={fields.length}
|
||||
key={template.id}
|
||||
documentFlow={documentFlow.fields}
|
||||
recipients={recipients}
|
||||
fields={fields}
|
||||
onSubmit={onAddFieldsFormSubmit}
|
||||
onAutoSave={onAddFieldsFormAutoSave}
|
||||
teamId={team?.id}
|
||||
/>
|
||||
</Stepper>
|
||||
|
||||
@ -9,10 +9,10 @@ import { trpc } from '@documenso/trpc/react';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||
|
||||
import { FolderGrid } from '~/components/general/folder/folder-grid';
|
||||
import { TemplateDropZoneWrapper } from '~/components/general/template/template-drop-zone-wrapper';
|
||||
import { TemplatesTable } from '~/components/tables/templates-table';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
import { appMetaTags } from '~/utils/meta';
|
||||
import { TemplateDropZoneWrapper } from '~/components/general/template/template-drop-zone-wrapper';
|
||||
|
||||
export function meta() {
|
||||
return appMetaTags('Templates');
|
||||
|
||||
@ -0,0 +1,293 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
|
||||
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
const setupDocumentAndNavigateToFieldsStep = async (page: Page) => {
|
||||
const { user, team } = await seedUser();
|
||||
const document = await seedBlankDocument(user, team.id);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/documents/${document.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||
|
||||
await page.getByRole('button', { name: 'Add signer' }).click();
|
||||
|
||||
await page.getByPlaceholder('Email').nth(1).fill('recipient2@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(1).fill('Recipient 2');
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
return { user, team, document };
|
||||
};
|
||||
|
||||
const triggerAutosave = async (page: Page) => {
|
||||
await page.locator('#document-flow-form-container').click();
|
||||
await page.locator('#document-flow-form-container').blur();
|
||||
|
||||
await page.waitForTimeout(5000);
|
||||
};
|
||||
|
||||
test.describe('AutoSave Fields Step', () => {
|
||||
test('should autosave the fields without advanced settings', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocumentAndNavigateToFieldsStep(page);
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Text' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 200,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
|
||||
|
||||
await page
|
||||
.getByTestId('field-advanced-settings-footer')
|
||||
.getByRole('button', { name: 'Cancel' })
|
||||
.click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 500,
|
||||
},
|
||||
});
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedFields = await getFieldsForDocument({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedFields.length).toBe(3);
|
||||
expect(retrievedFields[0].type).toBe('SIGNATURE');
|
||||
expect(retrievedFields[1].type).toBe('TEXT');
|
||||
expect(retrievedFields[2].type).toBe('SIGNATURE');
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the field deletion', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocumentAndNavigateToFieldsStep(page);
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Text' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 200,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
|
||||
|
||||
await page
|
||||
.getByTestId('field-advanced-settings-footer')
|
||||
.getByRole('button', { name: 'Cancel' })
|
||||
.click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 500,
|
||||
},
|
||||
});
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Recipient 1 (recipient1@documenso.com)' }).click();
|
||||
|
||||
await page.getByText('Text').nth(1).click();
|
||||
await page.getByRole('button', { name: 'Remove' }).click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedFields = await getFieldsForDocument({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedFields.length).toBe(2);
|
||||
expect(retrievedFields[0].type).toBe('SIGNATURE');
|
||||
expect(retrievedFields[1].type).toBe('SIGNATURE');
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the field duplication', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocumentAndNavigateToFieldsStep(page);
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Text' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 200,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
|
||||
|
||||
await page
|
||||
.getByTestId('field-advanced-settings-footer')
|
||||
.getByRole('button', { name: 'Cancel' })
|
||||
.click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 500,
|
||||
},
|
||||
});
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Recipient 1 (recipient1@documenso.com)' }).click();
|
||||
|
||||
await page.getByText('Signature').nth(1).click();
|
||||
await page.getByRole('button', { name: 'Duplicate', exact: true }).click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedFields = await getFieldsForDocument({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedFields.length).toBe(4);
|
||||
expect(retrievedFields[0].type).toBe('SIGNATURE');
|
||||
expect(retrievedFields[1].type).toBe('TEXT');
|
||||
expect(retrievedFields[2].type).toBe('SIGNATURE');
|
||||
expect(retrievedFields[3].type).toBe('SIGNATURE');
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the fields with advanced settings', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocumentAndNavigateToFieldsStep(page);
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Text' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 200,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('textbox', { name: 'Field label' }).fill('Test Field');
|
||||
await page.getByRole('textbox', { name: 'Field placeholder' }).fill('Test Placeholder');
|
||||
await page.getByRole('textbox', { name: 'Add text to the field' }).fill('Test Text');
|
||||
|
||||
await page
|
||||
.getByTestId('field-advanced-settings-footer')
|
||||
.getByRole('button', { name: 'Save' })
|
||||
.click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedFields = await getFieldsForDocument({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedFields.length).toBe(2);
|
||||
expect(retrievedFields[0].type).toBe('SIGNATURE');
|
||||
expect(retrievedFields[1].type).toBe('TEXT');
|
||||
|
||||
const textField = retrievedFields[1];
|
||||
expect(textField.fieldMeta).toBeDefined();
|
||||
|
||||
if (
|
||||
textField.fieldMeta &&
|
||||
typeof textField.fieldMeta === 'object' &&
|
||||
'type' in textField.fieldMeta
|
||||
) {
|
||||
expect(textField.fieldMeta.type).toBe('text');
|
||||
expect(textField.fieldMeta.label).toBe('Test Field');
|
||||
expect(textField.fieldMeta.placeholder).toBe('Test Placeholder');
|
||||
|
||||
if (textField.fieldMeta.type === 'text') {
|
||||
expect(textField.fieldMeta.text).toBe('Test Text');
|
||||
}
|
||||
} else {
|
||||
throw new Error('fieldMeta should be defined and contain advanced settings');
|
||||
}
|
||||
}).toPass();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,243 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
test.describe.configure({ mode: 'parallel', timeout: 60000 });
|
||||
|
||||
const setupDocument = async (page: Page) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const document = await seedBlankDocument(user, team.id);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/documents/${document.id}/edit`,
|
||||
});
|
||||
|
||||
return { user, team, document };
|
||||
};
|
||||
|
||||
const triggerAutosave = async (page: Page) => {
|
||||
await page.locator('#document-flow-form-container').click();
|
||||
await page.locator('#document-flow-form-container').blur();
|
||||
|
||||
await page.waitForTimeout(5000);
|
||||
};
|
||||
|
||||
test.describe('AutoSave Settings Step', () => {
|
||||
test('should autosave the title change', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocument(page);
|
||||
|
||||
const newDocumentTitle = 'New Document Title';
|
||||
|
||||
await page.getByRole('textbox', { name: 'Title *' }).fill(newDocumentTitle);
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrieved = await getDocumentById({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
await expect(page.getByRole('textbox', { name: 'Title *' })).toHaveValue(retrieved.title);
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the language change', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocument(page);
|
||||
|
||||
const newDocumentLanguage = 'French';
|
||||
const expectedLanguageCode = 'fr';
|
||||
|
||||
await page.getByRole('combobox').first().click();
|
||||
await page.getByRole('option', { name: newDocumentLanguage }).click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrieved = await getDocumentById({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrieved.documentMeta?.language).toBe(expectedLanguageCode);
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the document access change', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocument(page);
|
||||
|
||||
const access = 'Require account';
|
||||
const accessValue = 'ACCOUNT';
|
||||
|
||||
await page.getByRole('combobox').nth(1).click();
|
||||
await page.getByRole('option', { name: access }).click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrieved = await getDocumentById({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrieved.authOptions?.globalAccessAuth).toContain(accessValue);
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the external ID change', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocument(page);
|
||||
|
||||
const newExternalId = '1234567890';
|
||||
|
||||
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||
|
||||
await page.getByRole('textbox', { name: 'External ID' }).fill(newExternalId);
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrieved = await getDocumentById({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrieved.externalId).toBe(newExternalId);
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the allowed signature types change', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocument(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||
|
||||
await page.getByRole('combobox').nth(3).click();
|
||||
await page.getByRole('option', { name: 'Draw' }).click();
|
||||
await page.getByRole('option', { name: 'Type' }).click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrieved = await getDocumentById({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrieved.documentMeta?.drawSignatureEnabled).toBe(false);
|
||||
expect(retrieved.documentMeta?.typedSignatureEnabled).toBe(false);
|
||||
expect(retrieved.documentMeta?.uploadSignatureEnabled).toBe(true);
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the date format change', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocument(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||
|
||||
await page.getByRole('combobox').nth(4).click();
|
||||
await page.getByRole('option', { name: 'ISO 8601', exact: true }).click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrieved = await getDocumentById({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrieved.documentMeta?.dateFormat).toBe("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the timezone change', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocument(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||
|
||||
await page.getByRole('combobox').nth(5).click();
|
||||
await page.getByRole('option', { name: 'Europe/London' }).click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrieved = await getDocumentById({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrieved.documentMeta?.timezone).toBe('Europe/London');
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the redirect URL change', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocument(page);
|
||||
|
||||
const newRedirectUrl = 'https://documenso.com/test/';
|
||||
|
||||
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||
|
||||
await page.getByRole('textbox', { name: 'Redirect URL' }).fill(newRedirectUrl);
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrieved = await getDocumentById({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrieved.documentMeta?.redirectUrl).toBe(newRedirectUrl);
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave multiple field changes together', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocument(page);
|
||||
|
||||
const newTitle = 'Updated Document Title';
|
||||
await page.getByRole('textbox', { name: 'Title *' }).fill(newTitle);
|
||||
|
||||
await page.getByRole('combobox').first().click();
|
||||
await page.getByRole('option', { name: 'German' }).click();
|
||||
|
||||
await page.getByRole('combobox').nth(1).click();
|
||||
await page.getByRole('option', { name: 'Require account' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||
const newExternalId = 'MULTI-TEST-123';
|
||||
await page.getByRole('textbox', { name: 'External ID' }).fill(newExternalId);
|
||||
|
||||
await page.getByRole('combobox').nth(5).click();
|
||||
await page.getByRole('option', { name: 'Europe/Berlin' }).click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrieved = await getDocumentById({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrieved.title).toBe(newTitle);
|
||||
expect(retrieved.documentMeta?.language).toBe('de');
|
||||
expect(retrieved.authOptions?.globalAccessAuth).toContain('ACCOUNT');
|
||||
expect(retrieved.externalId).toBe(newExternalId);
|
||||
expect(retrieved.documentMeta?.timezone).toBe('Europe/Berlin');
|
||||
}).toPass();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,168 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
test.describe.configure({ mode: 'parallel', timeout: 60000 });
|
||||
|
||||
const setupDocumentAndNavigateToSignersStep = async (page: Page) => {
|
||||
const { user, team } = await seedUser();
|
||||
const document = await seedBlankDocument(user, team.id);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/documents/${document.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
return { user, team, document };
|
||||
};
|
||||
|
||||
const triggerAutosave = async (page: Page) => {
|
||||
await page.locator('#document-flow-form-container').click();
|
||||
await page.locator('#document-flow-form-container').blur();
|
||||
|
||||
await page.waitForTimeout(5000);
|
||||
};
|
||||
|
||||
const addSignerAndSave = async (page: Page) => {
|
||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||
|
||||
await triggerAutosave(page);
|
||||
};
|
||||
|
||||
test.describe('AutoSave Signers Step', () => {
|
||||
test('should autosave the signers addition', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocumentAndNavigateToSignersStep(page);
|
||||
|
||||
await addSignerAndSave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedRecipients = await getRecipientsForDocument({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedRecipients.length).toBe(1);
|
||||
expect(retrievedRecipients[0].email).toBe('recipient1@documenso.com');
|
||||
expect(retrievedRecipients[0].name).toBe('Recipient 1');
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the signer deletion', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocumentAndNavigateToSignersStep(page);
|
||||
|
||||
await addSignerAndSave(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Add myself' }).click();
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByTestId('remove-signer-button').first().click();
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedRecipients = await getRecipientsForDocument({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedRecipients.length).toBe(1);
|
||||
expect(retrievedRecipients[0].email).toBe(user.email);
|
||||
expect(retrievedRecipients[0].name).toBe(user.name);
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the signer update', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocumentAndNavigateToSignersStep(page);
|
||||
|
||||
await addSignerAndSave(page);
|
||||
|
||||
await page.getByPlaceholder('Name').fill('Documenso Manager');
|
||||
await page.getByPlaceholder('Email').fill('manager@documenso.com');
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Receives copy' }).click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedRecipients = await getRecipientsForDocument({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedRecipients.length).toBe(1);
|
||||
expect(retrievedRecipients[0].email).toBe('manager@documenso.com');
|
||||
expect(retrievedRecipients[0].name).toBe('Documenso Manager');
|
||||
expect(retrievedRecipients[0].role).toBe('CC');
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the signing order change', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocumentAndNavigateToSignersStep(page);
|
||||
|
||||
await addSignerAndSave(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Add signer' }).click();
|
||||
|
||||
await page.getByTestId('signer-email-input').nth(1).fill('recipient2@documenso.com');
|
||||
await page.getByLabel('Name').nth(1).fill('Recipient 2');
|
||||
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
await page.getByTestId('signer-email-input').nth(2).fill('recipient3@documenso.com');
|
||||
await page.getByLabel('Name').nth(2).fill('Recipient 3');
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByLabel('Enable signing order').check();
|
||||
await page.getByLabel('Allow signers to dictate next signer').check();
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByTestId('signing-order-input').nth(0).fill('3');
|
||||
await page.getByTestId('signing-order-input').nth(0).blur();
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByTestId('signing-order-input').nth(1).fill('1');
|
||||
await page.getByTestId('signing-order-input').nth(1).blur();
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByTestId('signing-order-input').nth(2).fill('2');
|
||||
await page.getByTestId('signing-order-input').nth(2).blur();
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedDocumentData = await getDocumentById({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
const retrievedRecipients = await getRecipientsForDocument({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedDocumentData.documentMeta?.signingOrder).toBe('SEQUENTIAL');
|
||||
expect(retrievedDocumentData.documentMeta?.allowDictateNextSigner).toBe(true);
|
||||
expect(retrievedRecipients.length).toBe(3);
|
||||
expect(retrievedRecipients[0].signingOrder).toBe(2);
|
||||
expect(retrievedRecipients[1].signingOrder).toBe(3);
|
||||
expect(retrievedRecipients[2].signingOrder).toBe(1);
|
||||
}).toPass();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,200 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
test.describe.configure({ mode: 'parallel', timeout: 60000 });
|
||||
|
||||
export const setupDocumentAndNavigateToSubjectStep = async (page: Page) => {
|
||||
const { user, team } = await seedUser();
|
||||
const document = await seedBlankDocument(user, team.id);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/documents/${document.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Distribute Document' })).toBeVisible();
|
||||
|
||||
return { user, team, document };
|
||||
};
|
||||
|
||||
export const triggerAutosave = async (page: Page) => {
|
||||
await page.locator('#document-flow-form-container').click();
|
||||
await page.locator('#document-flow-form-container').blur();
|
||||
|
||||
await page.waitForTimeout(5000);
|
||||
};
|
||||
|
||||
test.describe('AutoSave Subject Step', () => {
|
||||
test('should autosave the subject field', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocumentAndNavigateToSubjectStep(page);
|
||||
|
||||
const subject = 'Hello world!';
|
||||
|
||||
await page.getByRole('textbox', { name: 'Subject (Optional)' }).fill(subject);
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedDocumentData = await getDocumentById({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
await expect(page.getByRole('textbox', { name: 'Subject (Optional)' })).toHaveValue(
|
||||
retrievedDocumentData.documentMeta?.subject ?? '',
|
||||
);
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the message field', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocumentAndNavigateToSubjectStep(page);
|
||||
|
||||
const message = 'Please review and sign this important document. Thank you!';
|
||||
|
||||
await page.getByRole('textbox', { name: 'Message (Optional)' }).fill(message);
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedDocumentData = await getDocumentById({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
await expect(page.getByRole('textbox', { name: 'Message (Optional)' })).toHaveValue(
|
||||
retrievedDocumentData.documentMeta?.message ?? '',
|
||||
);
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the email settings checkboxes', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocumentAndNavigateToSubjectStep(page);
|
||||
|
||||
// Toggle some email settings checkboxes (randomly - some checked, some unchecked)
|
||||
await page.getByText('Send recipient signed email').click();
|
||||
await page.getByText('Send recipient removed email').click();
|
||||
await page.getByText('Send document completed email', { exact: true }).click();
|
||||
await page.getByText('Send document deleted email').click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedDocumentData = await getDocumentById({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
const emailSettings = retrievedDocumentData.documentMeta?.emailSettings;
|
||||
|
||||
await expect(page.getByText('Send recipient signed email')).toBeChecked({
|
||||
checked: emailSettings?.recipientSigned,
|
||||
});
|
||||
await expect(page.getByText('Send recipient removed email')).toBeChecked({
|
||||
checked: emailSettings?.recipientRemoved,
|
||||
});
|
||||
await expect(page.getByText('Send document completed email', { exact: true })).toBeChecked({
|
||||
checked: emailSettings?.documentCompleted,
|
||||
});
|
||||
await expect(page.getByText('Send document deleted email')).toBeChecked({
|
||||
checked: emailSettings?.documentDeleted,
|
||||
});
|
||||
|
||||
await expect(page.getByText('Send recipient signing request email')).toBeChecked({
|
||||
checked: emailSettings?.recipientSigningRequest,
|
||||
});
|
||||
await expect(page.getByText('Send document pending email')).toBeChecked({
|
||||
checked: emailSettings?.documentPending,
|
||||
});
|
||||
await expect(page.getByText('Send document completed email to the owner')).toBeChecked({
|
||||
checked: emailSettings?.ownerDocumentCompleted,
|
||||
});
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave all fields and settings together', async ({ page }) => {
|
||||
const { user, document, team } = await setupDocumentAndNavigateToSubjectStep(page);
|
||||
|
||||
const subject = 'Combined Test Subject - Please Sign';
|
||||
const message =
|
||||
'This is a comprehensive test message for autosave functionality. Please review and sign at your earliest convenience.';
|
||||
|
||||
await page.getByRole('textbox', { name: 'Subject (Optional)' }).fill(subject);
|
||||
await page.getByRole('textbox', { name: 'Message (Optional)' }).fill(message);
|
||||
|
||||
await page.getByText('Send recipient signed email').click();
|
||||
await page.getByText('Send recipient removed email').click();
|
||||
await page.getByText('Send document completed email', { exact: true }).click();
|
||||
await page.getByText('Send document deleted email').click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedDocumentData = await getDocumentById({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedDocumentData.documentMeta?.subject).toBe(subject);
|
||||
expect(retrievedDocumentData.documentMeta?.message).toBe(message);
|
||||
expect(retrievedDocumentData.documentMeta?.emailSettings).toBeDefined();
|
||||
|
||||
await expect(page.getByRole('textbox', { name: 'Subject (Optional)' })).toHaveValue(
|
||||
retrievedDocumentData.documentMeta?.subject ?? '',
|
||||
);
|
||||
await expect(page.getByRole('textbox', { name: 'Message (Optional)' })).toHaveValue(
|
||||
retrievedDocumentData.documentMeta?.message ?? '',
|
||||
);
|
||||
|
||||
await expect(page.getByText('Send recipient signed email')).toBeChecked({
|
||||
checked: retrievedDocumentData.documentMeta?.emailSettings?.recipientSigned,
|
||||
});
|
||||
await expect(page.getByText('Send recipient removed email')).toBeChecked({
|
||||
checked: retrievedDocumentData.documentMeta?.emailSettings?.recipientRemoved,
|
||||
});
|
||||
await expect(page.getByText('Send document completed email', { exact: true })).toBeChecked({
|
||||
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentCompleted,
|
||||
});
|
||||
await expect(page.getByText('Send document deleted email')).toBeChecked({
|
||||
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentDeleted,
|
||||
});
|
||||
|
||||
await expect(page.getByText('Send recipient signing request email')).toBeChecked({
|
||||
checked: retrievedDocumentData.documentMeta?.emailSettings?.recipientSigningRequest,
|
||||
});
|
||||
await expect(page.getByText('Send document pending email')).toBeChecked({
|
||||
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentPending,
|
||||
});
|
||||
await expect(page.getByText('Send document completed email to the owner')).toBeChecked({
|
||||
checked: retrievedDocumentData.documentMeta?.emailSettings?.ownerDocumentCompleted,
|
||||
});
|
||||
}).toPass();
|
||||
});
|
||||
});
|
||||
@ -534,9 +534,6 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip
|
||||
await page.getByLabel('Title').fill(documentTitle);
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
await page.getByLabel('Enable signing order').check();
|
||||
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
if (i > 1) {
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
@ -558,6 +555,9 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip
|
||||
.fill(`User ${i}`);
|
||||
}
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
await page.getByLabel('Enable signing order').check();
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
@ -0,0 +1,304 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
const setupTemplateAndNavigateToFieldsStep = async (page: Page) => {
|
||||
const { user, team } = await seedUser();
|
||||
const template = await seedBlankTemplate(user, team.id);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/templates/${template.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
await page.getByPlaceholder('Email').nth(1).fill('recipient2@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(1).fill('Recipient 2');
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
return { user, team, template };
|
||||
};
|
||||
|
||||
const triggerAutosave = async (page: Page) => {
|
||||
await page.locator('#document-flow-form-container').click();
|
||||
await page.locator('#document-flow-form-container').blur();
|
||||
|
||||
await page.waitForTimeout(5000);
|
||||
};
|
||||
|
||||
test.describe('AutoSave Fields Step', () => {
|
||||
test('should autosave the fields without advanced settings', async ({ page }) => {
|
||||
const { user, template, team } = await setupTemplateAndNavigateToFieldsStep(page);
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Text' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 200,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
|
||||
|
||||
await page
|
||||
.getByTestId('field-advanced-settings-footer')
|
||||
.getByRole('button', { name: 'Cancel' })
|
||||
.click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 500,
|
||||
},
|
||||
});
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedFields = await getTemplateById({
|
||||
id: template.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
const fields = retrievedFields.fields;
|
||||
|
||||
expect(fields.length).toBe(3);
|
||||
expect(fields[0].type).toBe('SIGNATURE');
|
||||
expect(fields[1].type).toBe('TEXT');
|
||||
expect(fields[2].type).toBe('SIGNATURE');
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the field deletion', async ({ page }) => {
|
||||
const { user, template, team } = await setupTemplateAndNavigateToFieldsStep(page);
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Text' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 200,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
|
||||
|
||||
await page
|
||||
.getByTestId('field-advanced-settings-footer')
|
||||
.getByRole('button', { name: 'Cancel' })
|
||||
.click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 500,
|
||||
},
|
||||
});
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Recipient 1 (recipient1@documenso.com)' }).click();
|
||||
|
||||
await page.getByText('Text').nth(1).click();
|
||||
await page.getByRole('button', { name: 'Remove' }).click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedFields = await getTemplateById({
|
||||
id: template.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
const fields = retrievedFields.fields;
|
||||
|
||||
expect(fields.length).toBe(2);
|
||||
expect(fields[0].type).toBe('SIGNATURE');
|
||||
expect(fields[1].type).toBe('SIGNATURE');
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the field duplication', async ({ page }) => {
|
||||
const { user, template, team } = await setupTemplateAndNavigateToFieldsStep(page);
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Text' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 200,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
|
||||
|
||||
await page
|
||||
.getByTestId('field-advanced-settings-footer')
|
||||
.getByRole('button', { name: 'Cancel' })
|
||||
.click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 500,
|
||||
},
|
||||
});
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Recipient 1 (recipient1@documenso.com)' }).click();
|
||||
|
||||
await page.getByText('Signature').nth(1).click();
|
||||
await page.getByRole('button', { name: 'Duplicate', exact: true }).click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedFields = await getTemplateById({
|
||||
id: template.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
const fields = retrievedFields.fields;
|
||||
|
||||
expect(fields.length).toBe(4);
|
||||
expect(fields[0].type).toBe('SIGNATURE');
|
||||
expect(fields[1].type).toBe('TEXT');
|
||||
expect(fields[2].type).toBe('SIGNATURE');
|
||||
expect(fields[3].type).toBe('SIGNATURE');
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the fields with advanced settings', async ({ page }) => {
|
||||
const { user, template, team } = await setupTemplateAndNavigateToFieldsStep(page);
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Text' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 200,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('textbox', { name: 'Field label' }).fill('Test Field');
|
||||
await page.getByRole('textbox', { name: 'Field placeholder' }).fill('Test Placeholder');
|
||||
await page.getByRole('textbox', { name: 'Add text to the field' }).fill('Test Text');
|
||||
|
||||
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
|
||||
|
||||
await page
|
||||
.getByTestId('field-advanced-settings-footer')
|
||||
.getByRole('button', { name: 'Save' })
|
||||
.click();
|
||||
|
||||
await page.waitForTimeout(2500);
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedTemplate = await getTemplateById({
|
||||
id: template.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
const fields = retrievedTemplate.fields;
|
||||
|
||||
expect(fields.length).toBe(2);
|
||||
expect(fields[0].type).toBe('SIGNATURE');
|
||||
expect(fields[1].type).toBe('TEXT');
|
||||
|
||||
const textField = fields[1];
|
||||
expect(textField.fieldMeta).toBeDefined();
|
||||
|
||||
if (
|
||||
textField.fieldMeta &&
|
||||
typeof textField.fieldMeta === 'object' &&
|
||||
'type' in textField.fieldMeta
|
||||
) {
|
||||
expect(textField.fieldMeta.type).toBe('text');
|
||||
expect(textField.fieldMeta.label).toBe('Test Field');
|
||||
expect(textField.fieldMeta.placeholder).toBe('Test Placeholder');
|
||||
|
||||
if (textField.fieldMeta.type === 'text') {
|
||||
expect(textField.fieldMeta.text).toBe('Test Text');
|
||||
}
|
||||
} else {
|
||||
throw new Error('fieldMeta should be defined and contain advanced settings');
|
||||
}
|
||||
}).toPass();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,244 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
test.describe.configure({ mode: 'parallel', timeout: 60000 });
|
||||
|
||||
const setupTemplate = async (page: Page) => {
|
||||
const { user, team } = await seedUser();
|
||||
const template = await seedBlankTemplate(user, team.id);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/templates/${template.id}/edit`,
|
||||
});
|
||||
|
||||
return { user, team, template };
|
||||
};
|
||||
|
||||
const triggerAutosave = async (page: Page) => {
|
||||
await page.locator('#document-flow-form-container').click();
|
||||
await page.locator('#document-flow-form-container').blur();
|
||||
|
||||
await page.waitForTimeout(5000);
|
||||
};
|
||||
|
||||
test.describe('AutoSave Settings Step - Templates', () => {
|
||||
test('should autosave the title change', async ({ page }) => {
|
||||
const { user, template, team } = await setupTemplate(page);
|
||||
|
||||
const newTemplateTitle = 'New Template Title';
|
||||
|
||||
await page.getByRole('textbox', { name: 'Title *' }).fill(newTemplateTitle);
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedTemplate = await getTemplateById({
|
||||
id: template.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
await expect(page.getByRole('textbox', { name: 'Title *' })).toHaveValue(
|
||||
retrievedTemplate.title,
|
||||
);
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the language change', async ({ page }) => {
|
||||
const { user, template, team } = await setupTemplate(page);
|
||||
|
||||
const newTemplateLanguage = 'French';
|
||||
const expectedLanguageCode = 'fr';
|
||||
|
||||
await page.getByRole('combobox').first().click();
|
||||
await page.getByRole('option', { name: newTemplateLanguage }).click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedTemplate = await getTemplateById({
|
||||
id: template.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedTemplate.templateMeta?.language).toBe(expectedLanguageCode);
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the template access change', async ({ page }) => {
|
||||
const { user, template, team } = await setupTemplate(page);
|
||||
|
||||
const access = 'Require account';
|
||||
const accessValue = 'ACCOUNT';
|
||||
|
||||
await page.getByRole('combobox').nth(1).click();
|
||||
await page.getByRole('option', { name: access }).click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedTemplate = await getTemplateById({
|
||||
id: template.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedTemplate.authOptions?.globalAccessAuth).toContain(accessValue);
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the external ID change', async ({ page }) => {
|
||||
const { user, template, team } = await setupTemplate(page);
|
||||
|
||||
const newExternalId = '1234567890';
|
||||
|
||||
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||
|
||||
await page.getByRole('textbox', { name: 'External ID' }).fill(newExternalId);
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedTemplate = await getTemplateById({
|
||||
id: template.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedTemplate.externalId).toBe(newExternalId);
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the allowed signature types change', async ({ page }) => {
|
||||
const { user, template, team } = await setupTemplate(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||
|
||||
await page.getByRole('combobox').nth(4).click();
|
||||
await page.getByRole('option', { name: 'Draw' }).click();
|
||||
await page.getByRole('option', { name: 'Type' }).click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedTemplate = await getTemplateById({
|
||||
id: template.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedTemplate.templateMeta?.drawSignatureEnabled).toBe(false);
|
||||
expect(retrievedTemplate.templateMeta?.typedSignatureEnabled).toBe(false);
|
||||
expect(retrievedTemplate.templateMeta?.uploadSignatureEnabled).toBe(true);
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the date format change', async ({ page }) => {
|
||||
const { user, template, team } = await setupTemplate(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||
|
||||
await page.getByRole('combobox').nth(5).click();
|
||||
await page.getByRole('option', { name: 'ISO 8601', exact: true }).click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedTemplate = await getTemplateById({
|
||||
id: template.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedTemplate.templateMeta?.dateFormat).toBe("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the timezone change', async ({ page }) => {
|
||||
const { user, template, team } = await setupTemplate(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||
|
||||
await page.getByRole('combobox').nth(6).click();
|
||||
await page.getByRole('option', { name: 'Europe/London' }).click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedTemplate = await getTemplateById({
|
||||
id: template.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedTemplate.templateMeta?.timezone).toBe('Europe/London');
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the redirect URL change', async ({ page }) => {
|
||||
const { user, template, team } = await setupTemplate(page);
|
||||
|
||||
const newRedirectUrl = 'https://documenso.com/test/';
|
||||
|
||||
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||
|
||||
await page.getByRole('textbox', { name: 'Redirect URL' }).fill(newRedirectUrl);
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedTemplate = await getTemplateById({
|
||||
id: template.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedTemplate.templateMeta?.redirectUrl).toBe(newRedirectUrl);
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave multiple field changes together', async ({ page }) => {
|
||||
const { user, template, team } = await setupTemplate(page);
|
||||
|
||||
const newTitle = 'Updated Template Title';
|
||||
await page.getByRole('textbox', { name: 'Title *' }).fill(newTitle);
|
||||
|
||||
await page.getByRole('combobox').first().click();
|
||||
await page.getByRole('option', { name: 'German' }).click();
|
||||
|
||||
await page.getByRole('combobox').nth(1).click();
|
||||
await page.getByRole('option', { name: 'Require account' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||
const newExternalId = 'MULTI-TEST-123';
|
||||
await page.getByRole('textbox', { name: 'External ID' }).fill(newExternalId);
|
||||
|
||||
await page.getByRole('combobox').nth(6).click();
|
||||
await page.getByRole('option', { name: 'Europe/Berlin' }).click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedTemplate = await getTemplateById({
|
||||
id: template.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedTemplate.title).toBe(newTitle);
|
||||
expect(retrievedTemplate.templateMeta?.language).toBe('de');
|
||||
expect(retrievedTemplate.authOptions?.globalAccessAuth).toContain('ACCOUNT');
|
||||
expect(retrievedTemplate.externalId).toBe(newExternalId);
|
||||
expect(retrievedTemplate.templateMeta?.timezone).toBe('Europe/Berlin');
|
||||
}).toPass();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,174 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { getRecipientsForTemplate } from '@documenso/lib/server-only/recipient/get-recipients-for-template';
|
||||
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
test.describe.configure({ mode: 'parallel', timeout: 60000 });
|
||||
|
||||
const setupTemplateAndNavigateToSignersStep = async (page: Page) => {
|
||||
const { user, team } = await seedUser();
|
||||
const template = await seedBlankTemplate(user, team.id);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/templates/${template.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
return { user, team, template };
|
||||
};
|
||||
|
||||
const triggerAutosave = async (page: Page) => {
|
||||
await page.locator('#document-flow-form-container').click();
|
||||
await page.locator('#document-flow-form-container').blur();
|
||||
|
||||
await page.waitForTimeout(5000);
|
||||
};
|
||||
|
||||
const addSignerAndSave = async (page: Page) => {
|
||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||
|
||||
await triggerAutosave(page);
|
||||
};
|
||||
|
||||
test.describe('AutoSave Signers Step - Templates', () => {
|
||||
test('should autosave the signers addition', async ({ page }) => {
|
||||
const { user, template, team } = await setupTemplateAndNavigateToSignersStep(page);
|
||||
|
||||
await addSignerAndSave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedRecipients = await getRecipientsForTemplate({
|
||||
templateId: template.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedRecipients.length).toBe(1);
|
||||
expect(retrievedRecipients[0].email).toBe('recipient1@documenso.com');
|
||||
expect(retrievedRecipients[0].name).toBe('Recipient 1');
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the signer deletion', async ({ page }) => {
|
||||
const { user, template, team } = await setupTemplateAndNavigateToSignersStep(page);
|
||||
|
||||
await addSignerAndSave(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Add myself' }).click();
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByTestId('remove-placeholder-recipient-button').first().click();
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedRecipients = await getRecipientsForTemplate({
|
||||
templateId: template.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedRecipients.length).toBe(1);
|
||||
expect(retrievedRecipients[0].email).toBe(user.email);
|
||||
expect(retrievedRecipients[0].name).toBe(user.name);
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the signer update', async ({ page }) => {
|
||||
const { user, template, team } = await setupTemplateAndNavigateToSignersStep(page);
|
||||
|
||||
await addSignerAndSave(page);
|
||||
|
||||
await page.getByPlaceholder('Name').fill('Documenso Manager');
|
||||
await page.getByPlaceholder('Email').fill('manager@documenso.com');
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Receives copy' }).click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedRecipients = await getRecipientsForTemplate({
|
||||
templateId: template.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedRecipients.length).toBe(1);
|
||||
expect(retrievedRecipients[0].email).toBe('manager@documenso.com');
|
||||
expect(retrievedRecipients[0].name).toBe('Documenso Manager');
|
||||
expect(retrievedRecipients[0].role).toBe('CC');
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('should autosave the signing order change', async ({ page }) => {
|
||||
const { user, template, team } = await setupTemplateAndNavigateToSignersStep(page);
|
||||
|
||||
await addSignerAndSave(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Add placeholder recipient' }).click();
|
||||
|
||||
await page
|
||||
.getByTestId('placeholder-recipient-email-input')
|
||||
.nth(1)
|
||||
.fill('recipient2@documenso.com');
|
||||
await page.getByTestId('placeholder-recipient-name-input').nth(1).fill('Recipient 2');
|
||||
|
||||
await page.getByRole('button', { name: 'Add placeholder recipient' }).click();
|
||||
|
||||
await page
|
||||
.getByTestId('placeholder-recipient-email-input')
|
||||
.nth(2)
|
||||
.fill('recipient3@documenso.com');
|
||||
await page.getByTestId('placeholder-recipient-name-input').nth(2).fill('Recipient 3');
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByLabel('Enable signing order').check();
|
||||
await page.getByLabel('Allow signers to dictate next signer').check();
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByTestId('placeholder-recipient-signing-order-input').nth(0).fill('3');
|
||||
await page.getByTestId('placeholder-recipient-signing-order-input').nth(0).blur();
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByTestId('placeholder-recipient-signing-order-input').nth(1).fill('1');
|
||||
await page.getByTestId('placeholder-recipient-signing-order-input').nth(1).blur();
|
||||
await triggerAutosave(page);
|
||||
|
||||
await page.getByTestId('placeholder-recipient-signing-order-input').nth(2).fill('2');
|
||||
await page.getByTestId('placeholder-recipient-signing-order-input').nth(2).blur();
|
||||
await triggerAutosave(page);
|
||||
|
||||
await expect(async () => {
|
||||
const retrievedTemplate = await getTemplateById({
|
||||
id: template.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
const retrievedRecipients = await getRecipientsForTemplate({
|
||||
templateId: template.id,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
expect(retrievedTemplate.templateMeta?.signingOrder).toBe('SEQUENTIAL');
|
||||
expect(retrievedTemplate.templateMeta?.allowDictateNextSigner).toBe(true);
|
||||
expect(retrievedRecipients.length).toBe(3);
|
||||
expect(retrievedRecipients[0].signingOrder).toBe(2);
|
||||
expect(retrievedRecipients[1].signingOrder).toBe(3);
|
||||
expect(retrievedRecipients[2].signingOrder).toBe(1);
|
||||
}).toPass();
|
||||
});
|
||||
});
|
||||
@ -17,7 +17,7 @@ export default defineConfig({
|
||||
testDir: './e2e',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: false,
|
||||
workers: 1,
|
||||
workers: 4,
|
||||
maxFailures: process.env.CI ? 1 : undefined,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
|
||||
31
packages/lib/client-only/hooks/use-autosave.ts
Normal file
31
packages/lib/client-only/hooks/use-autosave.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
export const useAutoSave = <T>(onSave: (data: T) => Promise<void>) => {
|
||||
const saveTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
const saveFormData = async (data: T) => {
|
||||
try {
|
||||
await onSave(data);
|
||||
} catch (error) {
|
||||
console.error('Auto-save failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleSave = useCallback((data: T) => {
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current);
|
||||
}
|
||||
|
||||
saveTimeoutRef.current = setTimeout(() => void saveFormData(data), 2000);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { scheduleSave };
|
||||
};
|
||||
@ -1,5 +1,7 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export interface GetRecipientsForTemplateOptions {
|
||||
templateId: number;
|
||||
userId: number;
|
||||
@ -14,21 +16,12 @@ export const getRecipientsForTemplate = async ({
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
templateId,
|
||||
template: teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
},
|
||||
template: {
|
||||
team: buildTeamWhereQuery({
|
||||
teamId,
|
||||
userId,
|
||||
}),
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
|
||||
@ -21,6 +21,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { prop, sortBy } from 'remeda';
|
||||
|
||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||
import { useAutoSave } from '@documenso/lib/client-only/hooks/use-autosave';
|
||||
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import {
|
||||
@ -83,6 +84,7 @@ export type AddFieldsFormProps = {
|
||||
recipients: Recipient[];
|
||||
fields: Field[];
|
||||
onSubmit: (_data: TAddFieldsFormSchema) => void;
|
||||
onAutoSave: (_data: TAddFieldsFormSchema) => Promise<void>;
|
||||
canGoBack?: boolean;
|
||||
isDocumentPdfLoaded: boolean;
|
||||
teamId: number;
|
||||
@ -94,6 +96,7 @@ export const AddFieldsFormPartial = ({
|
||||
recipients,
|
||||
fields,
|
||||
onSubmit,
|
||||
onAutoSave,
|
||||
canGoBack = false,
|
||||
isDocumentPdfLoaded,
|
||||
teamId,
|
||||
@ -590,6 +593,20 @@ export const AddFieldsFormPartial = ({
|
||||
}
|
||||
};
|
||||
|
||||
const { scheduleSave } = useAutoSave(onAutoSave);
|
||||
|
||||
const handleAutoSave = async () => {
|
||||
const isFormValid = await form.trigger();
|
||||
|
||||
if (!isFormValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = form.getValues();
|
||||
|
||||
scheduleSave(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{showAdvancedSettings && currentField ? (
|
||||
@ -603,7 +620,14 @@ export const AddFieldsFormPartial = ({
|
||||
fields={localFields}
|
||||
onAdvancedSettings={handleAdvancedSettings}
|
||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
||||
onSave={handleSavedFieldSettings}
|
||||
onSave={(fieldState) => {
|
||||
handleSavedFieldSettings(fieldState);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
onAutoSave={async (fieldState) => {
|
||||
handleSavedFieldSettings(fieldState);
|
||||
await handleAutoSave();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
@ -660,14 +684,26 @@ export const AddFieldsFormPartial = ({
|
||||
defaultWidth={DEFAULT_WIDTH_PX}
|
||||
passive={isFieldWithinBounds && !!selectedField}
|
||||
onFocus={() => setLastActiveField(field)}
|
||||
onBlur={() => setLastActiveField(null)}
|
||||
onBlur={() => {
|
||||
setLastActiveField(null);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
onMouseEnter={() => setLastActiveField(field)}
|
||||
onMouseLeave={() => setLastActiveField(null)}
|
||||
onResize={(options) => onFieldResize(options, index)}
|
||||
onMove={(options) => onFieldMove(options, index)}
|
||||
onRemove={() => remove(index)}
|
||||
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
|
||||
onDuplicateAllPages={() => onFieldCopy(null, { duplicateAll: true })}
|
||||
onRemove={() => {
|
||||
remove(index);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
onDuplicate={() => {
|
||||
onFieldCopy(null, { duplicate: true });
|
||||
void handleAutoSave();
|
||||
}}
|
||||
onDuplicateAllPages={() => {
|
||||
onFieldCopy(null, { duplicateAll: true });
|
||||
void handleAutoSave();
|
||||
}}
|
||||
onAdvancedSettings={() => {
|
||||
setCurrentField(field);
|
||||
handleAdvancedSettings();
|
||||
|
||||
@ -14,6 +14,7 @@ import { InfoIcon } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useAutoSave } from '@documenso/lib/client-only/hooks/use-autosave';
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
import { DOCUMENT_SIGNATURE_TYPES } from '@documenso/lib/constants/document';
|
||||
@ -79,6 +80,7 @@ export type AddSettingsFormProps = {
|
||||
document: TDocument;
|
||||
currentTeamMemberRole?: TeamMemberRole;
|
||||
onSubmit: (_data: TAddSettingsFormSchema) => void;
|
||||
onAutoSave: (_data: TAddSettingsFormSchema) => Promise<void>;
|
||||
};
|
||||
|
||||
export const AddSettingsFormPartial = ({
|
||||
@ -89,6 +91,7 @@ export const AddSettingsFormPartial = ({
|
||||
document,
|
||||
currentTeamMemberRole,
|
||||
onSubmit,
|
||||
onAutoSave,
|
||||
}: AddSettingsFormProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
@ -161,6 +164,28 @@ export const AddSettingsFormPartial = ({
|
||||
document.documentMeta?.timezone,
|
||||
]);
|
||||
|
||||
const { scheduleSave } = useAutoSave(onAutoSave);
|
||||
|
||||
const handleAutoSave = async () => {
|
||||
const isFormValid = await form.trigger();
|
||||
|
||||
if (!isFormValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = form.getValues();
|
||||
|
||||
/*
|
||||
* Parse the form data through the Zod schema to handle transformations
|
||||
* (like -1 -> undefined for the Document Global Auth Access)
|
||||
*/
|
||||
const parseResult = ZAddSettingsFormSchema.safeParse(formData);
|
||||
|
||||
if (parseResult.success) {
|
||||
scheduleSave(parseResult.data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocumentFlowFormContainerHeader
|
||||
@ -196,6 +221,7 @@ export const AddSettingsFormPartial = ({
|
||||
className="bg-background"
|
||||
{...field}
|
||||
disabled={document.status !== DocumentStatus.DRAFT || field.disabled}
|
||||
onBlur={handleAutoSave}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@ -227,9 +253,13 @@ export const AddSettingsFormPartial = ({
|
||||
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger className="bg-background">
|
||||
<SelectValue />
|
||||
@ -261,9 +291,13 @@ export const AddSettingsFormPartial = ({
|
||||
|
||||
<FormControl>
|
||||
<DocumentGlobalAuthAccessSelect
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
@ -286,7 +320,10 @@ export const AddSettingsFormPartial = ({
|
||||
canUpdateVisibility={canUpdateVisibility}
|
||||
currentTeamMemberRole={currentTeamMemberRole}
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
@ -307,9 +344,13 @@ export const AddSettingsFormPartial = ({
|
||||
|
||||
<FormControl>
|
||||
<DocumentGlobalAuthActionSelect
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
@ -347,7 +388,7 @@ export const AddSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input className="bg-background" {...field} />
|
||||
<Input className="bg-background" {...field} onBlur={handleAutoSave} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
@ -372,7 +413,10 @@ export const AddSettingsFormPartial = ({
|
||||
value: option.value,
|
||||
}))}
|
||||
selectedValues={field.value}
|
||||
onChange={field.onChange}
|
||||
onChange={(value) => {
|
||||
field.onChange(value);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
className="bg-background w-full"
|
||||
emptySelectionPlaceholder="Select signature types"
|
||||
/>
|
||||
@ -394,8 +438,12 @@ export const AddSettingsFormPartial = ({
|
||||
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
disabled={documentHasBeenSent}
|
||||
>
|
||||
<SelectTrigger className="bg-background">
|
||||
@ -430,8 +478,12 @@ export const AddSettingsFormPartial = ({
|
||||
<Combobox
|
||||
className="bg-background"
|
||||
options={TIME_ZONES}
|
||||
{...field}
|
||||
onChange={(value) => {
|
||||
value && field.onChange(value);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
value={field.value}
|
||||
onChange={(value) => value && field.onChange(value)}
|
||||
disabled={documentHasBeenSent}
|
||||
/>
|
||||
</FormControl>
|
||||
@ -462,7 +514,7 @@ export const AddSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input className="bg-background" {...field} />
|
||||
<Input className="bg-background" {...field} onBlur={handleAutoSave} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@ -14,6 +14,7 @@ import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { prop, sortBy } from 'remeda';
|
||||
|
||||
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||
import { useAutoSave } from '@documenso/lib/client-only/hooks/use-autosave';
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
|
||||
@ -55,6 +56,7 @@ export type AddSignersFormProps = {
|
||||
signingOrder?: DocumentSigningOrder | null;
|
||||
allowDictateNextSigner?: boolean;
|
||||
onSubmit: (_data: TAddSignersFormSchema) => void;
|
||||
onAutoSave: (_data: TAddSignersFormSchema) => Promise<void>;
|
||||
isDocumentPdfLoaded: boolean;
|
||||
};
|
||||
|
||||
@ -65,6 +67,7 @@ export const AddSignersFormPartial = ({
|
||||
signingOrder,
|
||||
allowDictateNextSigner,
|
||||
onSubmit,
|
||||
onAutoSave,
|
||||
isDocumentPdfLoaded,
|
||||
}: AddSignersFormProps) => {
|
||||
const { _ } = useLingui();
|
||||
@ -166,6 +169,29 @@ export const AddSignersFormPartial = ({
|
||||
name: 'signers',
|
||||
});
|
||||
|
||||
const emptySigners = useCallback(
|
||||
() => form.getValues('signers').filter((signer) => signer.email === ''),
|
||||
[form],
|
||||
);
|
||||
|
||||
const { scheduleSave } = useAutoSave(onAutoSave);
|
||||
|
||||
const handleAutoSave = async () => {
|
||||
if (emptySigners().length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isFormValid = await form.trigger();
|
||||
|
||||
if (!isFormValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = form.getValues();
|
||||
|
||||
scheduleSave(formData);
|
||||
};
|
||||
|
||||
const emptySignerIndex = watchedSigners.findIndex((signer) => !signer.name && !signer.email);
|
||||
const isUserAlreadyARecipient = watchedSigners.some(
|
||||
(signer) => signer.email.toLowerCase() === user?.email?.toLowerCase(),
|
||||
@ -216,24 +242,47 @@ export const AddSignersFormPartial = ({
|
||||
const formStateIndex = form.getValues('signers').findIndex((s) => s.formId === signer.formId);
|
||||
if (formStateIndex !== -1) {
|
||||
removeSigner(formStateIndex);
|
||||
|
||||
const updatedSigners = form.getValues('signers').filter((s) => s.formId !== signer.formId);
|
||||
form.setValue('signers', normalizeSigningOrders(updatedSigners));
|
||||
|
||||
form.setValue('signers', normalizeSigningOrders(updatedSigners), {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
void handleAutoSave();
|
||||
}
|
||||
};
|
||||
|
||||
const onAddSelfSigner = () => {
|
||||
if (emptySignerIndex !== -1) {
|
||||
setValue(`signers.${emptySignerIndex}.name`, user?.name ?? '');
|
||||
setValue(`signers.${emptySignerIndex}.email`, user?.email ?? '');
|
||||
} else {
|
||||
appendSigner({
|
||||
formId: nanoid(12),
|
||||
name: user?.name ?? '',
|
||||
email: user?.email ?? '',
|
||||
role: RecipientRole.SIGNER,
|
||||
actionAuth: [],
|
||||
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
||||
setValue(`signers.${emptySignerIndex}.name`, user?.name ?? '', {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
setValue(`signers.${emptySignerIndex}.email`, user?.email ?? '', {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
form.setFocus(`signers.${emptySignerIndex}.email`);
|
||||
} else {
|
||||
appendSigner(
|
||||
{
|
||||
formId: nanoid(12),
|
||||
name: user?.name ?? '',
|
||||
email: user?.email ?? '',
|
||||
role: RecipientRole.SIGNER,
|
||||
actionAuth: [],
|
||||
signingOrder:
|
||||
signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
||||
},
|
||||
{
|
||||
shouldFocus: true,
|
||||
},
|
||||
);
|
||||
|
||||
void form.trigger('signers');
|
||||
}
|
||||
};
|
||||
|
||||
@ -263,7 +312,10 @@ export const AddSignersFormPartial = ({
|
||||
signingOrder: !canRecipientBeModified(signer.nativeId) ? signer.signingOrder : index + 1,
|
||||
}));
|
||||
|
||||
form.setValue('signers', updatedSigners);
|
||||
form.setValue('signers', updatedSigners, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
const lastSigner = updatedSigners[updatedSigners.length - 1];
|
||||
if (lastSigner.role === RecipientRole.ASSISTANT) {
|
||||
@ -276,8 +328,10 @@ export const AddSignersFormPartial = ({
|
||||
}
|
||||
|
||||
await form.trigger('signers');
|
||||
|
||||
void handleAutoSave();
|
||||
},
|
||||
[form, canRecipientBeModified, watchedSigners, toast],
|
||||
[form, canRecipientBeModified, watchedSigners, handleAutoSave, toast],
|
||||
);
|
||||
|
||||
const handleRoleChange = useCallback(
|
||||
@ -287,7 +341,10 @@ export const AddSignersFormPartial = ({
|
||||
|
||||
// Handle parallel to sequential conversion for assistants
|
||||
if (role === RecipientRole.ASSISTANT && signingOrder === DocumentSigningOrder.PARALLEL) {
|
||||
form.setValue('signingOrder', DocumentSigningOrder.SEQUENTIAL);
|
||||
form.setValue('signingOrder', DocumentSigningOrder.SEQUENTIAL, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
toast({
|
||||
title: _(msg`Signing order is enabled.`),
|
||||
description: _(msg`You cannot add assistants when signing order is disabled.`),
|
||||
@ -302,7 +359,10 @@ export const AddSignersFormPartial = ({
|
||||
signingOrder: !canRecipientBeModified(signer.nativeId) ? signer.signingOrder : idx + 1,
|
||||
}));
|
||||
|
||||
form.setValue('signers', updatedSigners);
|
||||
form.setValue('signers', updatedSigners, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
if (role === RecipientRole.ASSISTANT && index === updatedSigners.length - 1) {
|
||||
toast({
|
||||
@ -341,7 +401,10 @@ export const AddSignersFormPartial = ({
|
||||
signingOrder: !canRecipientBeModified(s.nativeId) ? s.signingOrder : idx + 1,
|
||||
}));
|
||||
|
||||
form.setValue('signers', updatedSigners);
|
||||
form.setValue('signers', updatedSigners, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
if (signer.role === RecipientRole.ASSISTANT && newPosition === remainingSigners.length - 1) {
|
||||
toast({
|
||||
@ -364,9 +427,20 @@ export const AddSignersFormPartial = ({
|
||||
role: signer.role === RecipientRole.ASSISTANT ? RecipientRole.SIGNER : signer.role,
|
||||
}));
|
||||
|
||||
form.setValue('signers', updatedSigners);
|
||||
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL);
|
||||
form.setValue('allowDictateNextSigner', false);
|
||||
form.setValue('signers', updatedSigners, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
form.setValue('allowDictateNextSigner', false, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
void handleAutoSave();
|
||||
}, [form]);
|
||||
|
||||
return (
|
||||
@ -408,19 +482,39 @@ export const AddSignersFormPartial = ({
|
||||
|
||||
// If sequential signing is turned off, disable dictate next signer
|
||||
if (!checked) {
|
||||
form.setValue('allowDictateNextSigner', false);
|
||||
form.setValue('allowDictateNextSigner', false, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
}
|
||||
|
||||
void handleAutoSave();
|
||||
}}
|
||||
disabled={isSubmitting || hasDocumentBeenSent}
|
||||
disabled={isSubmitting || hasDocumentBeenSent || emptySigners().length !== 0}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormLabel
|
||||
htmlFor="signingOrder"
|
||||
className="text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
<Trans>Enable signing order</Trans>
|
||||
</FormLabel>
|
||||
<div className="flex items-center text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
<FormLabel
|
||||
htmlFor="signingOrder"
|
||||
className="text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
<Trans>Enable signing order</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-muted-foreground ml-1 cursor-help">
|
||||
<HelpCircle className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-80 p-4">
|
||||
<p>
|
||||
<Trans>Add 2 or more signers to enable signing order.</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@ -435,12 +529,15 @@ export const AddSignersFormPartial = ({
|
||||
{...field}
|
||||
id="allowDictateNextSigner"
|
||||
checked={value}
|
||||
onCheckedChange={field.onChange}
|
||||
onCheckedChange={(checked) => {
|
||||
field.onChange(checked);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
disabled={isSubmitting || hasDocumentBeenSent || !isSigningOrderSequential}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
<FormLabel
|
||||
htmlFor="allowDictateNextSigner"
|
||||
className="text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
@ -533,6 +630,7 @@ export const AddSignersFormPartial = ({
|
||||
<Input
|
||||
type="number"
|
||||
max={signers.length}
|
||||
data-testid="signing-order-input"
|
||||
className={cn(
|
||||
'w-full text-center',
|
||||
'[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none',
|
||||
@ -541,10 +639,12 @@ export const AddSignersFormPartial = ({
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
handleSigningOrderChange(index, e.target.value);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
field.onBlur();
|
||||
handleSigningOrderChange(index, e.target.value);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
disabled={
|
||||
snapshot.isDragging ||
|
||||
@ -588,7 +688,9 @@ export const AddSignersFormPartial = ({
|
||||
isSubmitting ||
|
||||
!canRecipientBeModified(signer.nativeId)
|
||||
}
|
||||
data-testid="signer-email-input"
|
||||
onKeyDown={onKeyDown}
|
||||
onBlur={handleAutoSave}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@ -626,6 +728,7 @@ export const AddSignersFormPartial = ({
|
||||
!canRecipientBeModified(signer.nativeId)
|
||||
}
|
||||
onKeyDown={onKeyDown}
|
||||
onBlur={handleAutoSave}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@ -668,6 +771,7 @@ export const AddSignersFormPartial = ({
|
||||
|
||||
<div className="col-span-2 flex gap-x-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`signers.${index}.role`}
|
||||
render={({ field }) => (
|
||||
<FormItem
|
||||
@ -681,10 +785,11 @@ export const AddSignersFormPartial = ({
|
||||
<RecipientRoleSelect
|
||||
{...field}
|
||||
isAssistantEnabled={isSigningOrderSequential}
|
||||
onValueChange={(value) =>
|
||||
onValueChange={(value) => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
handleRoleChange(index, value as RecipientRole)
|
||||
}
|
||||
handleRoleChange(index, value as RecipientRole);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
disabled={
|
||||
snapshot.isDragging ||
|
||||
isSubmitting ||
|
||||
@ -706,6 +811,7 @@ export const AddSignersFormPartial = ({
|
||||
'mb-6': form.formState.errors.signers?.[index],
|
||||
},
|
||||
)}
|
||||
data-testid="remove-signer-button"
|
||||
disabled={
|
||||
snapshot.isDragging ||
|
||||
isSubmitting ||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
@ -8,6 +10,7 @@ import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { useAutoSave } from '@documenso/lib/client-only/hooks/use-autosave';
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import type { TDocument } from '@documenso/lib/types/document';
|
||||
@ -60,6 +63,7 @@ export type AddSubjectFormProps = {
|
||||
fields: Field[];
|
||||
document: TDocument;
|
||||
onSubmit: (_data: TAddSubjectFormSchema) => void;
|
||||
onAutoSave: (_data: TAddSubjectFormSchema) => Promise<void>;
|
||||
isDocumentPdfLoaded: boolean;
|
||||
};
|
||||
|
||||
@ -69,6 +73,7 @@ export const AddSubjectFormPartial = ({
|
||||
fields: fields,
|
||||
document,
|
||||
onSubmit,
|
||||
onAutoSave,
|
||||
isDocumentPdfLoaded,
|
||||
}: AddSubjectFormProps) => {
|
||||
const { _ } = useLingui();
|
||||
@ -95,6 +100,8 @@ export const AddSubjectFormPartial = ({
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
trigger,
|
||||
getValues,
|
||||
formState: { isSubmitting },
|
||||
} = form;
|
||||
|
||||
@ -129,6 +136,35 @@ export const AddSubjectFormPartial = ({
|
||||
const onFormSubmit = handleSubmit(onSubmit);
|
||||
const { currentStep, totalSteps, previousStep } = useStep();
|
||||
|
||||
const { scheduleSave } = useAutoSave(onAutoSave);
|
||||
|
||||
const handleAutoSave = async () => {
|
||||
const isFormValid = await trigger();
|
||||
|
||||
if (!isFormValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = getValues();
|
||||
|
||||
scheduleSave(formData);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const container = window.document.getElementById('document-flow-form-container');
|
||||
|
||||
const handleBlur = () => {
|
||||
void handleAutoSave();
|
||||
};
|
||||
|
||||
if (container) {
|
||||
container.addEventListener('blur', handleBlur, true);
|
||||
return () => {
|
||||
container.removeEventListener('blur', handleBlur, true);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocumentFlowFormContainerHeader
|
||||
@ -185,7 +221,6 @@ export const AddSubjectFormPartial = ({
|
||||
<FormLabel>
|
||||
<Trans>Email Sender</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
|
||||
@ -6,6 +6,7 @@ import { useLingui } from '@lingui/react';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useAutoSave } from '@documenso/lib/client-only/hooks/use-autosave';
|
||||
import {
|
||||
type TBaseFieldMeta as BaseFieldMeta,
|
||||
type TCheckboxFieldMeta as CheckboxFieldMeta,
|
||||
@ -48,6 +49,7 @@ export type FieldAdvancedSettingsProps = {
|
||||
onAdvancedSettings?: () => void;
|
||||
isDocumentPdfLoaded?: boolean;
|
||||
onSave?: (fieldState: FieldMeta) => void;
|
||||
onAutoSave?: (fieldState: FieldMeta) => Promise<void>;
|
||||
};
|
||||
|
||||
export type FieldMetaKeys =
|
||||
@ -146,7 +148,16 @@ const getDefaultState = (fieldType: FieldType): FieldMeta => {
|
||||
|
||||
export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSettingsProps>(
|
||||
(
|
||||
{ title, description, field, fields, onAdvancedSettings, isDocumentPdfLoaded = true, onSave },
|
||||
{
|
||||
title,
|
||||
description,
|
||||
field,
|
||||
fields,
|
||||
onAdvancedSettings,
|
||||
isDocumentPdfLoaded = true,
|
||||
onSave,
|
||||
onAutoSave,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { _ } = useLingui();
|
||||
@ -177,6 +188,24 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [fieldMeta]);
|
||||
|
||||
const { scheduleSave } = useAutoSave(onAutoSave || (async () => {}));
|
||||
|
||||
const handleAutoSave = () => {
|
||||
if (errors.length === 0) {
|
||||
scheduleSave(fieldState);
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-save to localStorage and schedule remote save when fieldState changes
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(fieldState));
|
||||
handleAutoSave();
|
||||
} catch (error) {
|
||||
console.error('Failed to save to localStorage:', error);
|
||||
}
|
||||
}, [fieldState, localStorageKey, handleAutoSave]);
|
||||
|
||||
const handleFieldChange = (
|
||||
key: FieldMetaKeys,
|
||||
value:
|
||||
@ -325,7 +354,10 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
|
||||
)}
|
||||
</DocumentFlowFormContainerContent>
|
||||
|
||||
<DocumentFlowFormContainerFooter className="mt-auto">
|
||||
<DocumentFlowFormContainerFooter
|
||||
className="mt-auto"
|
||||
data-testid="field-advanced-settings-footer"
|
||||
>
|
||||
<DocumentFlowFormContainerActions
|
||||
goNextLabel={msg`Save`}
|
||||
goBackLabel={msg`Cancel`}
|
||||
|
||||
@ -21,6 +21,7 @@ import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||
import { useAutoSave } from '@documenso/lib/client-only/hooks/use-autosave';
|
||||
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
@ -73,6 +74,7 @@ export type AddTemplateFieldsFormProps = {
|
||||
recipients: Recipient[];
|
||||
fields: Field[];
|
||||
onSubmit: (_data: TAddTemplateFieldsFormSchema) => void;
|
||||
onAutoSave: (_data: TAddTemplateFieldsFormSchema) => Promise<void>;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
@ -81,6 +83,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
recipients,
|
||||
fields,
|
||||
onSubmit,
|
||||
onAutoSave,
|
||||
teamId,
|
||||
}: AddTemplateFieldsFormProps) => {
|
||||
const { _ } = useLingui();
|
||||
@ -121,6 +124,20 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
|
||||
const onFormSubmit = form.handleSubmit(onSubmit);
|
||||
|
||||
const { scheduleSave } = useAutoSave(onAutoSave);
|
||||
|
||||
const handleAutoSave = async () => {
|
||||
const isFormValid = await form.trigger();
|
||||
|
||||
if (!isFormValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = form.getValues();
|
||||
|
||||
scheduleSave(formData);
|
||||
};
|
||||
|
||||
const {
|
||||
append,
|
||||
remove,
|
||||
@ -160,6 +177,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
};
|
||||
|
||||
append(newField);
|
||||
void handleAutoSave();
|
||||
|
||||
return;
|
||||
}
|
||||
@ -187,6 +205,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
append(newField);
|
||||
});
|
||||
|
||||
void handleAutoSave();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -198,7 +217,15 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
});
|
||||
}
|
||||
},
|
||||
[append, lastActiveField, selectedSigner?.email, selectedSigner?.id, toast],
|
||||
[
|
||||
append,
|
||||
lastActiveField,
|
||||
selectedSigner?.email,
|
||||
selectedSigner?.id,
|
||||
selectedSigner?.token,
|
||||
toast,
|
||||
handleAutoSave,
|
||||
],
|
||||
);
|
||||
|
||||
const onFieldPaste = useCallback(
|
||||
@ -218,9 +245,18 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
pageX: copiedField.pageX + 3,
|
||||
pageY: copiedField.pageY + 3,
|
||||
});
|
||||
|
||||
void handleAutoSave();
|
||||
}
|
||||
},
|
||||
[append, fieldClipboard, selectedSigner?.email, selectedSigner?.id, selectedSigner?.token],
|
||||
[
|
||||
append,
|
||||
fieldClipboard,
|
||||
selectedSigner?.email,
|
||||
selectedSigner?.id,
|
||||
selectedSigner?.token,
|
||||
handleAutoSave,
|
||||
],
|
||||
);
|
||||
|
||||
useHotkeys(['ctrl+c', 'meta+c'], (evt) => onFieldCopy(evt));
|
||||
@ -378,8 +414,10 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
});
|
||||
|
||||
void handleAutoSave();
|
||||
},
|
||||
[getFieldPosition, localFields, update],
|
||||
[getFieldPosition, localFields, update, handleAutoSave],
|
||||
);
|
||||
|
||||
const onFieldMove = useCallback(
|
||||
@ -401,8 +439,10 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
pageX,
|
||||
pageY,
|
||||
});
|
||||
|
||||
void handleAutoSave();
|
||||
},
|
||||
[getFieldPosition, localFields, update],
|
||||
[getFieldPosition, localFields, update, handleAutoSave],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -504,6 +544,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
});
|
||||
|
||||
form.setValue('fields', updatedFields);
|
||||
void handleAutoSave();
|
||||
};
|
||||
|
||||
return (
|
||||
@ -519,6 +560,10 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
fields={localFields}
|
||||
onAdvancedSettings={handleAdvancedSettings}
|
||||
onSave={handleSavedFieldSettings}
|
||||
onAutoSave={async (fieldState) => {
|
||||
handleSavedFieldSettings(fieldState);
|
||||
await handleAutoSave();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
@ -566,12 +611,22 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
defaultWidth={DEFAULT_WIDTH_PX}
|
||||
passive={isFieldWithinBounds && !!selectedField}
|
||||
onFocus={() => setLastActiveField(field)}
|
||||
onBlur={() => setLastActiveField(null)}
|
||||
onBlur={() => {
|
||||
setLastActiveField(null);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
onResize={(options) => onFieldResize(options, index)}
|
||||
onMove={(options) => onFieldMove(options, index)}
|
||||
onRemove={() => remove(index)}
|
||||
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
|
||||
onDuplicateAllPages={() => onFieldCopy(null, { duplicateAll: true })}
|
||||
onRemove={() => {
|
||||
remove(index);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
onDuplicate={() => {
|
||||
onFieldCopy(null, { duplicate: true });
|
||||
}}
|
||||
onDuplicateAllPages={() => {
|
||||
onFieldCopy(null, { duplicateAll: true });
|
||||
}}
|
||||
onAdvancedSettings={() => {
|
||||
setCurrentField(field);
|
||||
handleAdvancedSettings();
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useId, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import type { DropResult, SensorAPI } from '@hello-pangea/dnd';
|
||||
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
|
||||
@ -12,6 +12,7 @@ import { motion } from 'framer-motion';
|
||||
import { GripVerticalIcon, HelpCircle, Link2Icon, Plus, Trash } from 'lucide-react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
|
||||
import { useAutoSave } from '@documenso/lib/client-only/hooks/use-autosave';
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
|
||||
@ -55,6 +56,7 @@ export type AddTemplatePlaceholderRecipientsFormProps = {
|
||||
allowDictateNextSigner?: boolean;
|
||||
templateDirectLink?: TemplateDirectLink | null;
|
||||
onSubmit: (_data: TAddTemplatePlacholderRecipientsFormSchema) => void;
|
||||
onAutoSave: (_data: TAddTemplatePlacholderRecipientsFormSchema) => Promise<void>;
|
||||
isDocumentPdfLoaded: boolean;
|
||||
};
|
||||
|
||||
@ -67,6 +69,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
allowDictateNextSigner,
|
||||
isDocumentPdfLoaded,
|
||||
onSubmit,
|
||||
onAutoSave,
|
||||
}: AddTemplatePlaceholderRecipientsFormProps) => {
|
||||
const initialId = useId();
|
||||
const $sensorApi = useRef<SensorAPI | null>(null);
|
||||
@ -123,15 +126,38 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
signers: generateDefaultFormSigners(),
|
||||
signingOrder: signingOrder || DocumentSigningOrder.PARALLEL,
|
||||
allowDictateNextSigner: allowDictateNextSigner ?? false,
|
||||
});
|
||||
const emptySigners = useCallback(
|
||||
() => form.getValues('signers').filter((signer) => signer.email === ''),
|
||||
[form],
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [recipients]);
|
||||
const { scheduleSave } = useAutoSave(onAutoSave);
|
||||
|
||||
const handleAutoSave = async () => {
|
||||
if (emptySigners().length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isFormValid = await form.trigger();
|
||||
|
||||
if (!isFormValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = form.getValues();
|
||||
|
||||
scheduleSave(formData);
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// form.reset({
|
||||
// signers: generateDefaultFormSigners(),
|
||||
// signingOrder: signingOrder || DocumentSigningOrder.PARALLEL,
|
||||
// allowDictateNextSigner: allowDictateNextSigner ?? false,
|
||||
// });
|
||||
|
||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// }, [recipients]);
|
||||
|
||||
// Always show advanced settings if any recipient has auth options.
|
||||
const alwaysShowAdvancedSettings = useMemo(() => {
|
||||
@ -204,7 +230,12 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
const onRemoveSigner = (index: number) => {
|
||||
removeSigner(index);
|
||||
const updatedSigners = signers.filter((_, idx) => idx !== index);
|
||||
form.setValue('signers', normalizeSigningOrders(updatedSigners));
|
||||
form.setValue('signers', normalizeSigningOrders(updatedSigners), {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
void handleAutoSave();
|
||||
};
|
||||
|
||||
const isSignerDirectRecipient = (
|
||||
@ -231,7 +262,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
signingOrder: index + 1,
|
||||
}));
|
||||
|
||||
form.setValue('signers', updatedSigners);
|
||||
form.setValue('signers', updatedSigners, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
const lastSigner = updatedSigners[updatedSigners.length - 1];
|
||||
if (lastSigner.role === RecipientRole.ASSISTANT) {
|
||||
@ -244,8 +278,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
}
|
||||
|
||||
await form.trigger('signers');
|
||||
|
||||
void handleAutoSave();
|
||||
},
|
||||
[form, watchedSigners, toast],
|
||||
[form, watchedSigners, toast, handleAutoSave],
|
||||
);
|
||||
|
||||
const handleSigningOrderChange = useCallback(
|
||||
@ -273,7 +309,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
signingOrder: idx + 1,
|
||||
}));
|
||||
|
||||
form.setValue('signers', updatedSigners);
|
||||
form.setValue('signers', updatedSigners, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
if (signer.role === RecipientRole.ASSISTANT && newPosition === remainingSigners.length - 1) {
|
||||
toast({
|
||||
@ -283,8 +322,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
void handleAutoSave();
|
||||
},
|
||||
[form, toast],
|
||||
[form, toast, handleAutoSave],
|
||||
);
|
||||
|
||||
const handleRoleChange = useCallback(
|
||||
@ -294,7 +335,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
|
||||
// Handle parallel to sequential conversion for assistants
|
||||
if (role === RecipientRole.ASSISTANT && signingOrder === DocumentSigningOrder.PARALLEL) {
|
||||
form.setValue('signingOrder', DocumentSigningOrder.SEQUENTIAL);
|
||||
form.setValue('signingOrder', DocumentSigningOrder.SEQUENTIAL, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
toast({
|
||||
title: _(msg`Signing order is enabled.`),
|
||||
description: _(msg`You cannot add assistants when signing order is disabled.`),
|
||||
@ -309,7 +353,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
signingOrder: idx + 1,
|
||||
}));
|
||||
|
||||
form.setValue('signers', updatedSigners);
|
||||
form.setValue('signers', updatedSigners, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
if (role === RecipientRole.ASSISTANT && index === updatedSigners.length - 1) {
|
||||
toast({
|
||||
@ -319,8 +366,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
void handleAutoSave();
|
||||
},
|
||||
[form, toast],
|
||||
[form, toast, handleAutoSave],
|
||||
);
|
||||
|
||||
const [showSigningOrderConfirmation, setShowSigningOrderConfirmation] = useState(false);
|
||||
@ -334,10 +383,21 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
role: signer.role === RecipientRole.ASSISTANT ? RecipientRole.SIGNER : signer.role,
|
||||
}));
|
||||
|
||||
form.setValue('signers', updatedSigners);
|
||||
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL);
|
||||
form.setValue('allowDictateNextSigner', false);
|
||||
}, [form]);
|
||||
form.setValue('signers', updatedSigners, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
form.setValue('allowDictateNextSigner', false, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
void handleAutoSave();
|
||||
}, [form, handleAutoSave]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -382,8 +442,13 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
|
||||
// If sequential signing is turned off, disable dictate next signer
|
||||
if (!checked) {
|
||||
form.setValue('allowDictateNextSigner', false);
|
||||
form.setValue('allowDictateNextSigner', false, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
}
|
||||
|
||||
void handleAutoSave();
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
@ -409,7 +474,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
{...field}
|
||||
id="allowDictateNextSigner"
|
||||
checked={value}
|
||||
onCheckedChange={field.onChange}
|
||||
onCheckedChange={(checked) => {
|
||||
field.onChange(checked);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
disabled={isSubmitting || !isSigningOrderSequential}
|
||||
/>
|
||||
</FormControl>
|
||||
@ -500,6 +568,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
<Input
|
||||
type="number"
|
||||
max={signers.length}
|
||||
data-testid="placeholder-recipient-signing-order-input"
|
||||
className={cn(
|
||||
'w-full text-center',
|
||||
'[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none',
|
||||
@ -558,6 +627,8 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
signers[index].email === user?.email ||
|
||||
isSignerDirectRecipient(signer)
|
||||
}
|
||||
onBlur={handleAutoSave}
|
||||
data-testid="placeholder-recipient-email-input"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@ -592,6 +663,8 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
signers[index].email === user?.email ||
|
||||
isSignerDirectRecipient(signer)
|
||||
}
|
||||
onBlur={handleAutoSave}
|
||||
data-testid="placeholder-recipient-name-input"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@ -633,10 +706,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
<FormControl>
|
||||
<RecipientRoleSelect
|
||||
{...field}
|
||||
onValueChange={(value) =>
|
||||
onValueChange={(value) => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
handleRoleChange(index, value as RecipientRole)
|
||||
}
|
||||
handleRoleChange(index, value as RecipientRole);
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
hideCCRecipients={isSignerDirectRecipient(signer)}
|
||||
/>
|
||||
@ -672,6 +745,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
className="col-span-1 mt-auto inline-flex h-10 w-10 items-center justify-center text-slate-500 hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={isSubmitting || signers.length === 1}
|
||||
onClick={() => onRemoveSigner(index)}
|
||||
data-testid="remove-placeholder-recipient-button"
|
||||
>
|
||||
<Trash className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
@ -9,6 +9,7 @@ import { InfoIcon } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useAutoSave } from '@documenso/lib/client-only/hooks/use-autosave';
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
import {
|
||||
@ -83,6 +84,7 @@ export type AddTemplateSettingsFormProps = {
|
||||
template: TTemplate;
|
||||
currentTeamMemberRole?: TeamMemberRole;
|
||||
onSubmit: (_data: TAddTemplateSettingsFormSchema) => void;
|
||||
onAutoSave: (_data: TAddTemplateSettingsFormSchema) => Promise<void>;
|
||||
};
|
||||
|
||||
export const AddTemplateSettingsFormPartial = ({
|
||||
@ -93,6 +95,7 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
template,
|
||||
currentTeamMemberRole,
|
||||
onSubmit,
|
||||
onAutoSave,
|
||||
}: AddTemplateSettingsFormProps) => {
|
||||
const { t, i18n } = useLingui();
|
||||
|
||||
@ -160,6 +163,28 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
}
|
||||
}, [form, form.setValue, form.formState.touchedFields.meta?.timezone]);
|
||||
|
||||
const { scheduleSave } = useAutoSave(onAutoSave);
|
||||
|
||||
const handleAutoSave = async () => {
|
||||
const isFormValid = await form.trigger();
|
||||
|
||||
if (!isFormValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = form.getValues();
|
||||
|
||||
/*
|
||||
* Parse the form data through the Zod schema to handle transformations
|
||||
* (like -1 -> undefined for the Document Global Auth Access)
|
||||
*/
|
||||
const parseResult = ZAddTemplateSettingsFormSchema.safeParse(formData);
|
||||
|
||||
if (parseResult.success) {
|
||||
scheduleSave(parseResult.data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocumentFlowFormContainerHeader
|
||||
@ -191,7 +216,7 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input className="bg-background" {...field} />
|
||||
<Input className="bg-background" {...field} onBlur={handleAutoSave} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -219,7 +244,13 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Select {...field} onValueChange={field.onChange}>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="bg-background">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@ -250,9 +281,13 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
|
||||
<FormControl>
|
||||
<DocumentGlobalAuthAccessSelect
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
@ -275,7 +310,10 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
canUpdateVisibility={canUpdateVisibility}
|
||||
currentTeamMemberRole={currentTeamMemberRole}
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
@ -334,7 +372,13 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Select {...field} onValueChange={field.onChange}>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="bg-background text-muted-foreground">
|
||||
<SelectValue data-testid="documentDistributionMethodSelectValue" />
|
||||
</SelectTrigger>
|
||||
@ -371,7 +415,10 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
value: option.value,
|
||||
}))}
|
||||
selectedValues={field.value}
|
||||
onChange={field.onChange}
|
||||
onChange={(value) => {
|
||||
field.onChange(value);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
className="bg-background w-full"
|
||||
emptySelectionPlaceholder="Select signature types"
|
||||
/>
|
||||
@ -395,9 +442,13 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
|
||||
<FormControl>
|
||||
<DocumentGlobalAuthActionSelect
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
@ -488,7 +539,7 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
<Input {...field} onBlur={handleAutoSave} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
@ -515,7 +566,11 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Textarea className="bg-background h-16 resize-none" {...field} />
|
||||
<Textarea
|
||||
className="bg-background h-16 resize-none"
|
||||
{...field}
|
||||
onBlur={handleAutoSave}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
@ -525,7 +580,12 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
|
||||
<DocumentEmailCheckboxes
|
||||
value={emailSettings}
|
||||
onChange={(value) => form.setValue('meta.emailSettings', value)}
|
||||
onChange={(value) => {
|
||||
form.setValue('meta.emailSettings', value, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
void handleAutoSave();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
@ -563,7 +623,7 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input className="bg-background" {...field} />
|
||||
<Input className="bg-background" {...field} onBlur={handleAutoSave} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
@ -581,7 +641,13 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Select {...field} onValueChange={field.onChange}>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="bg-background">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@ -615,7 +681,10 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
className="bg-background time-zone-field"
|
||||
options={TIME_ZONES}
|
||||
{...field}
|
||||
onChange={(value) => value && field.onChange(value)}
|
||||
onChange={(value) => {
|
||||
value && field.onChange(value);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@ -645,7 +714,7 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input className="bg-background" {...field} />
|
||||
<Input className="bg-background" {...field} onBlur={handleAutoSave} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
Reference in New Issue
Block a user