feat: add signature configurations (#1710)

Add ability to enable or disable allowed signature types:
- Drawn
- Typed
- Uploaded

**Tabbed style signature dialog**

![image](https://github.com/user-attachments/assets/a816fab6-b071-42a5-bb5c-6d4a2572431e)

**Document settings**

![image](https://github.com/user-attachments/assets/f0c1bff1-6be1-4c87-b384-1666fa25d7a6)

**Team preferences**

![image](https://github.com/user-attachments/assets/8767b05e-1463-4087-8672-f3f43d8b0f2c)

- Add multiselect to select allowed signatures in document and templates
settings tab
- Add multiselect to select allowed signatures in teams preferences
- Removed "Enable typed signatures" from document/template edit page
- Refactored signature pad to use tabs instead of an all in one
signature pad

Added E2E tests to check settings are applied correctly for documents
and templates
This commit is contained in:
David Nguyen
2025-03-24 15:25:29 +11:00
parent 231f51bd1f
commit 063fd32f18
85 changed files with 3141 additions and 1316 deletions

View File

@ -19,12 +19,15 @@ import {
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
export const ZProfileFormSchema = z.object({
name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
signature: z.string().min(1, 'Signature Pad cannot be empty'),
name: z
.string()
.trim()
.min(1, { message: msg`Please enter a valid name.`.id }),
signature: z.string().min(1, { message: msg`Signature Pad cannot be empty.`.id }),
});
export const ZTwoFactorAuthTokenSchema = z.object({
@ -109,22 +112,20 @@ export const ProfileForm = ({ className }: ProfileFormProps) => {
</Label>
<Input id="email" type="email" className="bg-muted mt-2" value={user.email} disabled />
</div>
<FormField
control={form.control}
name="signature"
render={({ field: { onChange } }) => (
render={({ field: { onChange, value } }) => (
<FormItem>
<FormLabel>
<Trans>Signature</Trans>
</FormLabel>
<FormControl>
<SignaturePad
className="h-44 w-full"
<SignaturePadDialog
disabled={isSubmitting}
containerClassName={cn('rounded-lg border bg-background')}
defaultValue={user.signature ?? undefined}
value={value}
onChange={(v) => onChange(v ?? '')}
allowTypedSignature={true}
/>
</FormControl>
<FormMessage />
@ -134,7 +135,7 @@ export const ProfileForm = ({ className }: ProfileFormProps) => {
</fieldset>
<Button type="submit" loading={isSubmitting} className="self-end">
{isSubmitting ? <Trans>Updating profile...</Trans> : <Trans>Update profile</Trans>}
<Trans>Update profile</Trans>
</Button>
</form>
</Form>

View File

@ -30,7 +30,7 @@ import {
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { PasswordInput } from '@documenso/ui/primitives/password-input';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { UserProfileSkeleton } from '~/components/general/user-profile-skeleton';
@ -353,16 +353,15 @@ export const SignUpForm = ({
<FormField
control={form.control}
name="signature"
render={({ field: { onChange } }) => (
render={({ field: { onChange, value } }) => (
<FormItem>
<FormLabel>
<Trans>Sign Here</Trans>
</FormLabel>
<FormControl>
<SignaturePad
className="h-36 w-full"
<SignaturePadDialog
disabled={isSubmitting}
containerClassName="mt-2 rounded-lg border bg-background"
value={value}
onChange={(v) => onChange(v ?? '')}
/>
</FormControl>

View File

@ -308,7 +308,7 @@ export function TeamBrandingPreferencesForm({ team, settings }: TeamBrandingPref
<div className="flex flex-row justify-end space-x-4">
<Button type="submit" loading={form.formState.isSubmitting}>
<Trans>Save</Trans>
<Trans>Update</Trans>
</Button>
</div>
</fieldset>

View File

@ -8,12 +8,15 @@ import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { DOCUMENT_SIGNATURE_TYPES, DocumentSignatureType } from '@documenso/lib/constants/document';
import {
SUPPORTED_LANGUAGES,
SUPPORTED_LANGUAGE_CODES,
isValidLanguageCode,
} from '@documenso/lib/constants/i18n';
import { extractTeamSignatureSettings } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import { DocumentSignatureSettingsTooltip } from '@documenso/ui/components/document/document-signature-settings-tooltip';
import { Alert } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import {
@ -23,7 +26,9 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { MultiSelectCombobox } from '@documenso/ui/primitives/multi-select-combobox';
import {
Select,
SelectContent,
@ -38,8 +43,10 @@ const ZTeamDocumentPreferencesFormSchema = z.object({
documentVisibility: z.nativeEnum(DocumentVisibility),
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES),
includeSenderDetails: z.boolean(),
typedSignatureEnabled: z.boolean(),
includeSigningCertificate: z.boolean(),
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(1, {
message: msg`At least one signature type must be enabled`.id,
}),
});
type TTeamDocumentPreferencesFormSchema = z.infer<typeof ZTeamDocumentPreferencesFormSchema>;
@ -69,8 +76,8 @@ export const TeamDocumentPreferencesForm = ({
? settings?.documentLanguage
: 'en',
includeSenderDetails: settings?.includeSenderDetails ?? false,
typedSignatureEnabled: settings?.typedSignatureEnabled ?? true,
includeSigningCertificate: settings?.includeSigningCertificate ?? true,
signatureTypes: extractTeamSignatureSettings(settings),
},
resolver: zodResolver(ZTeamDocumentPreferencesFormSchema),
});
@ -84,7 +91,7 @@ export const TeamDocumentPreferencesForm = ({
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
signatureTypes,
} = data;
await updateTeamDocumentPreferences({
@ -93,8 +100,10 @@ export const TeamDocumentPreferencesForm = ({
documentVisibility,
documentLanguage,
includeSenderDetails,
typedSignatureEnabled,
includeSigningCertificate,
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
},
});
@ -190,6 +199,44 @@ export const TeamDocumentPreferencesForm = ({
)}
/>
<FormField
control={form.control}
name="signatureTypes"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel className="flex flex-row items-center">
<Trans>Default Signature Settings</Trans>
<DocumentSignatureSettingsTooltip />
</FormLabel>
<FormControl>
<MultiSelectCombobox
options={Object.values(DOCUMENT_SIGNATURE_TYPES).map((option) => ({
label: _(option.label),
value: option.value,
}))}
selectedValues={field.value}
onChange={field.onChange}
className="bg-background w-full"
enableSearch={false}
emptySelectionPlaceholder="Select signature types"
testId="signature-types-combobox"
/>
</FormControl>
{form.formState.errors.signatureTypes ? (
<FormMessage />
) : (
<FormDescription>
<Trans>
Controls which signatures are allowed to be used when signing a document.
</Trans>
</FormDescription>
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name="includeSenderDetails"
@ -238,36 +285,6 @@ export const TeamDocumentPreferencesForm = ({
)}
/>
<FormField
control={form.control}
name="typedSignatureEnabled"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>
<Trans>Enable Typed Signature</Trans>
</FormLabel>
<div>
<FormControl className="block">
<Switch
ref={field.ref}
name={field.name}
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</div>
<FormDescription>
<Trans>
Controls whether the recipients can sign the documents using a typed signature.
Enable or disable the typed signature globally.
</Trans>
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="includeSigningCertificate"
@ -301,7 +318,7 @@ export const TeamDocumentPreferencesForm = ({
<div className="flex flex-row justify-end space-x-4">
<Button type="submit" loading={form.formState.isSubmitting}>
<Trans>Save</Trans>
<Trans>Update</Trans>
</Button>
</div>
</fieldset>