mirror of
https://github.com/documenso/documenso.git
synced 2025-11-24 21:51:40 +10:00
feat: add signature configurations (#1710)
Add ability to enable or disable allowed signature types: - Drawn - Typed - Uploaded **Tabbed style signature dialog**  **Document settings**  **Team preferences**  ## Changes Made - 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 ## Testing Performed Added E2E tests to check settings are applied correctly for documents and templates
This commit is contained in:
@ -34,3 +34,29 @@ export const DOCUMENT_DISTRIBUTION_METHODS: Record<string, DocumentDistributionM
|
||||
description: msg`None`,
|
||||
},
|
||||
} satisfies Record<DocumentDistributionMethod, DocumentDistributionMethodTypeData>;
|
||||
|
||||
export enum DocumentSignatureType {
|
||||
DRAW = 'draw',
|
||||
TYPE = 'type',
|
||||
UPLOAD = 'upload',
|
||||
}
|
||||
|
||||
type DocumentSignatureTypeData = {
|
||||
label: MessageDescriptor;
|
||||
value: DocumentSignatureType;
|
||||
};
|
||||
|
||||
export const DOCUMENT_SIGNATURE_TYPES = {
|
||||
[DocumentSignatureType.DRAW]: {
|
||||
label: msg`Draw`,
|
||||
value: DocumentSignatureType.DRAW,
|
||||
},
|
||||
[DocumentSignatureType.TYPE]: {
|
||||
label: msg`Type`,
|
||||
value: DocumentSignatureType.TYPE,
|
||||
},
|
||||
[DocumentSignatureType.UPLOAD]: {
|
||||
label: msg`Upload`,
|
||||
value: DocumentSignatureType.UPLOAD,
|
||||
},
|
||||
} satisfies Record<DocumentSignatureType, DocumentSignatureTypeData>;
|
||||
|
||||
4
packages/lib/constants/signatures.ts
Normal file
4
packages/lib/constants/signatures.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const SIGNATURE_CANVAS_DPI = 2;
|
||||
export const SIGNATURE_MIN_COVERAGE_THRESHOLD = 0.01;
|
||||
|
||||
export const isBase64Image = (value: string) => value.startsWith('data:image/png;base64,');
|
||||
@ -23,6 +23,8 @@ const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
brandingHidePoweredBy: z.boolean(),
|
||||
teamId: z.number(),
|
||||
typedSignatureEnabled: z.boolean(),
|
||||
uploadSignatureEnabled: z.boolean(),
|
||||
drawSignatureEnabled: z.boolean(),
|
||||
})
|
||||
.nullish(),
|
||||
}),
|
||||
|
||||
@ -26,6 +26,8 @@ export type CreateDocumentMetaOptions = {
|
||||
signingOrder?: DocumentSigningOrder;
|
||||
distributionMethod?: DocumentDistributionMethod;
|
||||
typedSignatureEnabled?: boolean;
|
||||
uploadSignatureEnabled?: boolean;
|
||||
drawSignatureEnabled?: boolean;
|
||||
language?: SupportedLanguageCodes;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
@ -44,6 +46,8 @@ export const upsertDocumentMeta = async ({
|
||||
emailSettings,
|
||||
distributionMethod,
|
||||
typedSignatureEnabled,
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
language,
|
||||
requestMetadata,
|
||||
}: CreateDocumentMetaOptions) => {
|
||||
@ -96,6 +100,8 @@ export const upsertDocumentMeta = async ({
|
||||
emailSettings,
|
||||
distributionMethod,
|
||||
typedSignatureEnabled,
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
language,
|
||||
},
|
||||
update: {
|
||||
@ -109,6 +115,8 @@ export const upsertDocumentMeta = async ({
|
||||
emailSettings,
|
||||
distributionMethod,
|
||||
typedSignatureEnabled,
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
language,
|
||||
},
|
||||
});
|
||||
|
||||
@ -158,6 +158,10 @@ export const createDocumentV2 = async ({
|
||||
language: meta?.language || team?.teamGlobalSettings?.documentLanguage,
|
||||
typedSignatureEnabled:
|
||||
meta?.typedSignatureEnabled ?? team?.teamGlobalSettings?.typedSignatureEnabled,
|
||||
uploadSignatureEnabled:
|
||||
meta?.uploadSignatureEnabled ?? team?.teamGlobalSettings?.uploadSignatureEnabled,
|
||||
drawSignatureEnabled:
|
||||
meta?.drawSignatureEnabled ?? team?.teamGlobalSettings?.drawSignatureEnabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -128,8 +128,10 @@ export const createDocument = async ({
|
||||
documentMeta: {
|
||||
create: {
|
||||
language: team?.teamGlobalSettings?.documentLanguage,
|
||||
typedSignatureEnabled: team?.teamGlobalSettings?.typedSignatureEnabled,
|
||||
timezone: timezone,
|
||||
typedSignatureEnabled: team?.teamGlobalSettings?.typedSignatureEnabled ?? true,
|
||||
uploadSignatureEnabled: team?.teamGlobalSettings?.uploadSignatureEnabled ?? true,
|
||||
drawSignatureEnabled: team?.teamGlobalSettings?.drawSignatureEnabled ?? true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -201,7 +201,7 @@ export const signFieldWithToken = async ({
|
||||
throw new Error('Signature field must have a signature');
|
||||
}
|
||||
|
||||
if (isSignatureField && !documentMeta?.typedSignatureEnabled && typedSignature) {
|
||||
if (isSignatureField && documentMeta?.typedSignatureEnabled === false && typedSignature) {
|
||||
throw new Error('Typed signatures are not allowed. Please draw your signature');
|
||||
}
|
||||
|
||||
|
||||
@ -1,73 +0,0 @@
|
||||
import type { DocumentVisibility } from '@prisma/client';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamGlobalSettingsSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamGlobalSettingsSchema';
|
||||
|
||||
import type { SupportedLanguageCodes } from '../../constants/i18n';
|
||||
|
||||
export type UpdateTeamDocumentSettingsOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
|
||||
settings: {
|
||||
documentVisibility: DocumentVisibility;
|
||||
documentLanguage: SupportedLanguageCodes;
|
||||
includeSenderDetails: boolean;
|
||||
typedSignatureEnabled: boolean;
|
||||
includeSigningCertificate: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export const ZUpdateTeamDocumentSettingsResponseSchema = TeamGlobalSettingsSchema;
|
||||
|
||||
export type TUpdateTeamDocumentSettingsResponse = z.infer<
|
||||
typeof ZUpdateTeamDocumentSettingsResponseSchema
|
||||
>;
|
||||
|
||||
export const updateTeamDocumentSettings = async ({
|
||||
userId,
|
||||
teamId,
|
||||
settings,
|
||||
}: UpdateTeamDocumentSettingsOptions): Promise<TUpdateTeamDocumentSettingsResponse> => {
|
||||
const {
|
||||
documentVisibility,
|
||||
documentLanguage,
|
||||
includeSenderDetails,
|
||||
includeSigningCertificate,
|
||||
typedSignatureEnabled,
|
||||
} = settings;
|
||||
|
||||
const member = await prisma.teamMember.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!member || member.role !== TeamMemberRole.ADMIN) {
|
||||
throw new Error('You do not have permission to update this team.');
|
||||
}
|
||||
|
||||
return await prisma.teamGlobalSettings.upsert({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
create: {
|
||||
teamId,
|
||||
documentVisibility,
|
||||
documentLanguage,
|
||||
includeSenderDetails,
|
||||
typedSignatureEnabled,
|
||||
includeSigningCertificate,
|
||||
},
|
||||
update: {
|
||||
documentVisibility,
|
||||
documentLanguage,
|
||||
includeSenderDetails,
|
||||
typedSignatureEnabled,
|
||||
includeSigningCertificate,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -324,6 +324,9 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
language: metaLanguage,
|
||||
signingOrder: metaSigningOrder,
|
||||
distributionMethod: template.templateMeta?.distributionMethod,
|
||||
typedSignatureEnabled: template.templateMeta?.typedSignatureEnabled,
|
||||
uploadSignatureEnabled: template.templateMeta?.uploadSignatureEnabled,
|
||||
drawSignatureEnabled: template.templateMeta?.drawSignatureEnabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -96,6 +96,9 @@ export const createDocumentFromTemplateLegacy = async ({
|
||||
signingOrder: template.templateMeta?.signingOrder ?? undefined,
|
||||
language:
|
||||
template.templateMeta?.language || template.team?.teamGlobalSettings?.documentLanguage,
|
||||
typedSignatureEnabled: template.templateMeta?.typedSignatureEnabled,
|
||||
uploadSignatureEnabled: template.templateMeta?.uploadSignatureEnabled,
|
||||
drawSignatureEnabled: template.templateMeta?.drawSignatureEnabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -82,8 +82,10 @@ export type CreateDocumentFromTemplateOptions = {
|
||||
signingOrder?: DocumentSigningOrder;
|
||||
language?: SupportedLanguageCodes;
|
||||
distributionMethod?: DocumentDistributionMethod;
|
||||
typedSignatureEnabled?: boolean;
|
||||
emailSettings?: TDocumentEmailSettings;
|
||||
typedSignatureEnabled?: boolean;
|
||||
uploadSignatureEnabled?: boolean;
|
||||
drawSignatureEnabled?: boolean;
|
||||
};
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
@ -404,6 +406,10 @@ export const createDocumentFromTemplate = async ({
|
||||
template.team?.teamGlobalSettings?.documentLanguage,
|
||||
typedSignatureEnabled:
|
||||
override?.typedSignatureEnabled ?? template.templateMeta?.typedSignatureEnabled,
|
||||
uploadSignatureEnabled:
|
||||
override?.uploadSignatureEnabled ?? template.templateMeta?.uploadSignatureEnabled,
|
||||
drawSignatureEnabled:
|
||||
override?.drawSignatureEnabled ?? template.templateMeta?.drawSignatureEnabled,
|
||||
},
|
||||
},
|
||||
recipients: {
|
||||
|
||||
@ -4,6 +4,8 @@ import { prisma } from '@documenso/prisma';
|
||||
import { TemplateSchema } from '@documenso/prisma/generated/zod/modelSchema//TemplateSchema';
|
||||
import type { TCreateTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
export type CreateTemplateOptions = TCreateTemplateMutationSchema & {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
@ -19,8 +21,10 @@ export const createTemplate = async ({
|
||||
teamId,
|
||||
templateDocumentDataId,
|
||||
}: CreateTemplateOptions) => {
|
||||
let team = null;
|
||||
|
||||
if (teamId) {
|
||||
await prisma.team.findFirstOrThrow({
|
||||
team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
@ -29,7 +33,14 @@ export const createTemplate = async ({
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.template.create({
|
||||
@ -38,6 +49,14 @@ export const createTemplate = async ({
|
||||
userId,
|
||||
templateDocumentDataId,
|
||||
teamId,
|
||||
templateMeta: {
|
||||
create: {
|
||||
language: team?.teamGlobalSettings?.documentLanguage,
|
||||
typedSignatureEnabled: team?.teamGlobalSettings?.typedSignatureEnabled ?? true,
|
||||
uploadSignatureEnabled: team?.teamGlobalSettings?.uploadSignatureEnabled ?? true,
|
||||
drawSignatureEnabled: team?.teamGlobalSettings?.drawSignatureEnabled ?? true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
53
packages/lib/types/document-meta.ts
Normal file
53
packages/lib/types/document-meta.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
|
||||
|
||||
/**
|
||||
* The full document response schema.
|
||||
*
|
||||
* Mainly used for returning a single document from the API.
|
||||
*/
|
||||
export const ZDocumentMetaSchema = DocumentMetaSchema.pick({
|
||||
signingOrder: true,
|
||||
distributionMethod: true,
|
||||
id: true,
|
||||
subject: true,
|
||||
message: true,
|
||||
timezone: true,
|
||||
password: true,
|
||||
dateFormat: true,
|
||||
documentId: true,
|
||||
redirectUrl: true,
|
||||
typedSignatureEnabled: true,
|
||||
uploadSignatureEnabled: true,
|
||||
drawSignatureEnabled: true,
|
||||
language: true,
|
||||
emailSettings: true,
|
||||
});
|
||||
|
||||
export type TDocumentMeta = z.infer<typeof ZDocumentMetaSchema>;
|
||||
|
||||
/**
|
||||
* If you update this, you must also update the schema.prisma @default value for
|
||||
* - Template meta
|
||||
* - Document meta
|
||||
*/
|
||||
export const ZDocumentSignatureSettingsSchema = z
|
||||
.object({
|
||||
typedSignatureEnabled: z.boolean(),
|
||||
uploadSignatureEnabled: z.boolean(),
|
||||
drawnSignatureEnabled: z.boolean(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
return (
|
||||
data.typedSignatureEnabled || data.uploadSignatureEnabled || data.drawnSignatureEnabled
|
||||
);
|
||||
},
|
||||
{
|
||||
message: msg`At least one signature type must be enabled`.id,
|
||||
},
|
||||
);
|
||||
|
||||
export type TDocumentSignatureSettings = z.infer<typeof ZDocumentSignatureSettingsSchema>;
|
||||
@ -51,6 +51,8 @@ export const ZDocumentSchema = DocumentSchema.pick({
|
||||
documentId: true,
|
||||
redirectUrl: true,
|
||||
typedSignatureEnabled: true,
|
||||
uploadSignatureEnabled: true,
|
||||
drawSignatureEnabled: true,
|
||||
language: true,
|
||||
emailSettings: true,
|
||||
}).nullable(),
|
||||
|
||||
@ -45,6 +45,8 @@ export const ZTemplateSchema = TemplateSchema.pick({
|
||||
dateFormat: true,
|
||||
signingOrder: true,
|
||||
typedSignatureEnabled: true,
|
||||
uploadSignatureEnabled: true,
|
||||
drawSignatureEnabled: true,
|
||||
distributionMethod: true,
|
||||
templateId: true,
|
||||
redirectUrl: true,
|
||||
|
||||
@ -47,6 +47,8 @@ export const ZWebhookDocumentMetaSchema = z.object({
|
||||
redirectUrl: z.string().nullable(),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||
typedSignatureEnabled: z.boolean(),
|
||||
uploadSignatureEnabled: z.boolean(),
|
||||
drawSignatureEnabled: z.boolean(),
|
||||
language: z.string(),
|
||||
distributionMethod: z.nativeEnum(DocumentDistributionMethod),
|
||||
emailSettings: z.any().nullable(),
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
||||
import { DocumentSignatureType } from '../constants/document';
|
||||
import type { TEAM_MEMBER_ROLE_MAP } from '../constants/teams';
|
||||
import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../constants/teams';
|
||||
|
||||
@ -44,3 +45,31 @@ export const isTeamRoleWithinUserHierarchy = (
|
||||
) => {
|
||||
return TEAM_MEMBER_ROLE_HIERARCHY[currentUserRole].some((i) => i === roleToCheck);
|
||||
};
|
||||
|
||||
export const extractTeamSignatureSettings = (
|
||||
settings?: {
|
||||
typedSignatureEnabled: boolean;
|
||||
drawSignatureEnabled: boolean;
|
||||
uploadSignatureEnabled: boolean;
|
||||
} | null,
|
||||
) => {
|
||||
if (!settings) {
|
||||
return [DocumentSignatureType.TYPE, DocumentSignatureType.UPLOAD, DocumentSignatureType.DRAW];
|
||||
}
|
||||
|
||||
const signatureTypes: DocumentSignatureType[] = [];
|
||||
|
||||
if (settings.typedSignatureEnabled) {
|
||||
signatureTypes.push(DocumentSignatureType.TYPE);
|
||||
}
|
||||
|
||||
if (settings.drawSignatureEnabled) {
|
||||
signatureTypes.push(DocumentSignatureType.DRAW);
|
||||
}
|
||||
|
||||
if (settings.uploadSignatureEnabled) {
|
||||
signatureTypes.push(DocumentSignatureType.UPLOAD);
|
||||
}
|
||||
|
||||
return signatureTypes;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user