feat: add template enhancements (#1154)

## Description

General enhancements for templates.

## Changes Made

Added the following changes to the template flow:
- Allow adding document meta settings
- Allow adding email settings
- Allow adding document access & action authentication
- Allow adding recipient action authentication
- Save the state between template steps similar to how it works for
documents

Other changes:
- Extract common fields between document and template flows
- Remove the title field from "Use template" since we now have it as
part of the template flow
- Add new API endpoint for generating templates

## Testing Performed

Added E2E tests for templates and creating documents from templates
This commit is contained in:
David Nguyen
2024-05-10 19:45:19 +07:00
committed by GitHub
parent 8f9c07aa8e
commit 2f86bb523b
38 changed files with 2103 additions and 510 deletions

View File

@ -1,21 +1,32 @@
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { prisma } from '@documenso/prisma';
import type { RecipientRole } from '@documenso/prisma/client';
import type { Recipient } from '@documenso/prisma/client';
import { RecipientRole } from '@documenso/prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
import {
type TRecipientActionAuthTypes,
ZRecipientAuthOptionsSchema,
} from '../../types/document-auth';
import { nanoid } from '../../universal/id';
import { createRecipientAuthOptions } from '../../utils/document-auth';
export type SetRecipientsForTemplateOptions = {
userId: number;
teamId?: number;
templateId: number;
recipients: {
id?: number;
email: string;
name: string;
role: RecipientRole;
actionAuth?: TRecipientActionAuthTypes | null;
}[];
};
export const setRecipientsForTemplate = async ({
userId,
teamId,
templateId,
recipients,
}: SetRecipientsForTemplateOptions) => {
@ -43,6 +54,23 @@ export const setRecipientsForTemplate = async ({
throw new Error('Template not found');
}
const recipientsHaveActionAuth = recipients.some((recipient) => recipient.actionAuth);
// Check if user has permission to set the global action auth.
if (recipientsHaveActionAuth) {
const isDocumentEnterprise = await isUserEnterprise({
userId,
teamId,
});
if (!isDocumentEnterprise) {
throw new AppError(
AppErrorCode.UNAUTHORIZED,
'You do not have permission to set the action auth',
);
}
}
const normalizedRecipients = recipients.map((recipient) => ({
...recipient,
email: recipient.email.toLowerCase(),
@ -74,31 +102,59 @@ export const setRecipientsForTemplate = async ({
};
});
const persistedRecipients = await prisma.$transaction(
// Disabling as wrapping promises here causes type issues
// eslint-disable-next-line @typescript-eslint/promise-function-async
linkedRecipients.map((recipient) =>
prisma.recipient.upsert({
where: {
id: recipient._persisted?.id ?? -1,
templateId,
},
update: {
name: recipient.name,
email: recipient.email,
role: recipient.role,
templateId,
},
create: {
name: recipient.name,
email: recipient.email,
role: recipient.role,
token: nanoid(),
templateId,
},
const persistedRecipients = await prisma.$transaction(async (tx) => {
return await Promise.all(
linkedRecipients.map(async (recipient) => {
let authOptions = ZRecipientAuthOptionsSchema.parse(recipient._persisted?.authOptions);
if (recipient.actionAuth !== undefined) {
authOptions = createRecipientAuthOptions({
accessAuth: authOptions.accessAuth,
actionAuth: recipient.actionAuth,
});
}
const upsertedRecipient = await tx.recipient.upsert({
where: {
id: recipient._persisted?.id ?? -1,
templateId,
},
update: {
name: recipient.name,
email: recipient.email,
role: recipient.role,
templateId,
authOptions,
},
create: {
name: recipient.name,
email: recipient.email,
role: recipient.role,
token: nanoid(),
templateId,
authOptions,
},
});
const recipientId = upsertedRecipient.id;
// Clear all fields if the recipient role is changed to a type that cannot have fields.
if (
recipient._persisted &&
recipient._persisted.role !== recipient.role &&
(recipient.role === RecipientRole.CC || recipient.role === RecipientRole.VIEWER)
) {
await tx.field.deleteMany({
where: {
recipientId,
},
});
}
return upsertedRecipient;
}),
),
);
);
});
if (removedRecipients.length > 0) {
await prisma.recipient.deleteMany({
@ -110,5 +166,17 @@ export const setRecipientsForTemplate = async ({
});
}
return persistedRecipients;
// Filter out recipients that have been removed or have been updated.
const filteredRecipients: Recipient[] = existingRecipients.filter((recipient) => {
const isRemoved = removedRecipients.find(
(removedRecipient) => removedRecipient.id === recipient.id,
);
const isUpdated = persistedRecipients.find(
(persistedRecipient) => persistedRecipient.id === recipient.id,
);
return !isRemoved && !isUpdated;
});
return [...filteredRecipients, ...persistedRecipients];
};