mirror of
https://github.com/documenso/documenso.git
synced 2026-07-24 17:04:12 +10:00
feat: add organisation template type (#2611)
This commit is contained in:
@@ -36,6 +36,9 @@ export const duplicateEnvelope = async ({ id, userId, teamId }: DuplicateEnvelop
|
||||
title: true,
|
||||
userId: true,
|
||||
internalVersion: true,
|
||||
templateType: true,
|
||||
publicTitle: true,
|
||||
publicDescription: true,
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: {
|
||||
@@ -87,6 +90,11 @@ export const duplicateEnvelope = async ({ id, userId, teamId }: DuplicateEnvelop
|
||||
}),
|
||||
]);
|
||||
|
||||
const duplicatedTemplateType =
|
||||
envelope.templateType === 'ORGANISATION' && envelope.teamId !== teamId
|
||||
? 'PRIVATE'
|
||||
: envelope.templateType ?? undefined;
|
||||
|
||||
const duplicatedEnvelope = await prisma.envelope.create({
|
||||
data: {
|
||||
id: prefixedId('envelope'),
|
||||
@@ -99,6 +107,9 @@ export const duplicateEnvelope = async ({ id, userId, teamId }: DuplicateEnvelop
|
||||
documentMetaId: createdDocumentMeta.id,
|
||||
authOptions: envelope.authOptions || undefined,
|
||||
visibility: envelope.visibility,
|
||||
templateType: duplicatedTemplateType,
|
||||
publicTitle: envelope.publicTitle ?? undefined,
|
||||
publicDescription: envelope.publicDescription ?? undefined,
|
||||
source:
|
||||
envelope.type === EnvelopeType.DOCUMENT ? DocumentSource.DOCUMENT : DocumentSource.TEMPLATE,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import type { EnvelopeType } from '@prisma/client';
|
||||
import type { EnvelopeType, Prisma } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ import { incrementDocumentId } from '../envelope/increment-id';
|
||||
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { getOrganisationTemplateWhereInput } from './get-organisation-template-by-id';
|
||||
|
||||
type FinalRecipient = Pick<
|
||||
Recipient,
|
||||
@@ -312,29 +313,43 @@ export const createDocumentFromTemplate = async ({
|
||||
attachments,
|
||||
formValues,
|
||||
}: CreateDocumentFromTemplateOptions) => {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
const templateInclude = {
|
||||
recipients: {
|
||||
include: {
|
||||
fields: true,
|
||||
},
|
||||
},
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
} as const;
|
||||
|
||||
const { envelopeWhereInput, team: callerTeam } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const template = await prisma.envelope.findUnique({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: {
|
||||
include: {
|
||||
fields: true,
|
||||
},
|
||||
},
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
const [teamTemplate, organisationTemplate] = await Promise.all([
|
||||
prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: templateInclude,
|
||||
}),
|
||||
prisma.envelope.findFirst({
|
||||
where: getOrganisationTemplateWhereInput({
|
||||
id,
|
||||
organisationId: callerTeam.organisationId,
|
||||
teamRole: callerTeam.currentTeamRole,
|
||||
}),
|
||||
include: templateInclude,
|
||||
}),
|
||||
]);
|
||||
|
||||
const template = teamTemplate ?? organisationTemplate;
|
||||
|
||||
if (!template) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
@@ -541,7 +556,7 @@ export const createDocumentFromTemplate = async ({
|
||||
templateId: legacyTemplateId, // The template this envelope was created from.
|
||||
userId,
|
||||
folderId,
|
||||
teamId: template.teamId,
|
||||
teamId,
|
||||
title: finalEnvelopeTitle,
|
||||
envelopeItems: {
|
||||
createMany: {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { EnvelopeType, type Prisma, TemplateType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import { type FindResultResponse } from '../../types/search-params';
|
||||
import { getMemberRoles } from '../team/get-member-roles';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export type FindOrganisationTemplatesOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
};
|
||||
|
||||
export const findOrganisationTemplates = async ({
|
||||
userId,
|
||||
teamId,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
}: FindOrganisationTemplatesOptions) => {
|
||||
const [team, { teamRole }] = await Promise.all([
|
||||
getTeamById({ teamId, userId }),
|
||||
getMemberRoles({
|
||||
teamId,
|
||||
reference: {
|
||||
type: 'User',
|
||||
id: userId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const where: Prisma.EnvelopeWhereInput = {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
templateType: TemplateType.ORGANISATION,
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[teamRole],
|
||||
},
|
||||
team: {
|
||||
organisationId: team.organisationId,
|
||||
},
|
||||
};
|
||||
|
||||
const templateInclude = {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
fields: true,
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
directLink: {
|
||||
select: {
|
||||
token: true,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.envelope.findMany({
|
||||
where,
|
||||
include: templateInclude,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
}),
|
||||
prisma.envelope.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
data,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
@@ -24,8 +24,6 @@ export const findTemplates = async ({
|
||||
perPage = 10,
|
||||
folderId,
|
||||
}: FindTemplatesOptions) => {
|
||||
const whereFilter: Prisma.EnvelopeWhereInput[] = [];
|
||||
|
||||
const { teamRole } = await getMemberRoles({
|
||||
teamId,
|
||||
reference: {
|
||||
@@ -34,63 +32,55 @@ export const findTemplates = async ({
|
||||
},
|
||||
});
|
||||
|
||||
whereFilter.push(
|
||||
{ teamId },
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[teamRole],
|
||||
const where: Prisma.EnvelopeWhereInput = {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
templateType: type,
|
||||
AND: [
|
||||
{ teamId },
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[teamRole],
|
||||
},
|
||||
},
|
||||
},
|
||||
{ userId, teamId },
|
||||
],
|
||||
},
|
||||
);
|
||||
{ userId, teamId },
|
||||
],
|
||||
},
|
||||
folderId ? { folderId } : { folderId: null },
|
||||
],
|
||||
};
|
||||
|
||||
if (folderId) {
|
||||
whereFilter.push({ folderId });
|
||||
} else {
|
||||
whereFilter.push({ folderId: null });
|
||||
}
|
||||
const templateInclude = {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
fields: true,
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
directLink: {
|
||||
select: {
|
||||
token: true,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.envelope.findMany({
|
||||
where: {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
templateType: type,
|
||||
AND: whereFilter,
|
||||
},
|
||||
include: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
fields: true,
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
directLink: {
|
||||
select: {
|
||||
token: true,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where,
|
||||
include: templateInclude,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
}),
|
||||
prisma.envelope.count({
|
||||
where: {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
templateType: type,
|
||||
AND: whereFilter,
|
||||
},
|
||||
}),
|
||||
prisma.envelope.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import type { Prisma, TeamMemberRole } from '@prisma/client';
|
||||
import { EnvelopeType, TemplateType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||
import { getMemberRoles } from '../team/get-member-roles';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export type GetOrganisationTemplateByIdOptions = {
|
||||
id: EnvelopeIdOptions;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an organisation template by ID.
|
||||
*
|
||||
* This validates that the caller's team belongs to the same organisation as the template's team,
|
||||
* that the template is of type ORGANISATION, and that the template's visibility is permitted
|
||||
* for the caller's role on their own team.
|
||||
*/
|
||||
export const getOrganisationTemplateById = async ({
|
||||
id,
|
||||
userId,
|
||||
teamId,
|
||||
}: GetOrganisationTemplateByIdOptions) => {
|
||||
const [callerTeam, { teamRole }] = await Promise.all([
|
||||
getTeamById({ teamId, userId }),
|
||||
getMemberRoles({
|
||||
teamId,
|
||||
reference: {
|
||||
type: 'User',
|
||||
id: userId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
...unsafeBuildEnvelopeIdQuery(id, EnvelopeType.TEMPLATE),
|
||||
templateType: TemplateType.ORGANISATION,
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[teamRole],
|
||||
},
|
||||
team: {
|
||||
organisationId: callerTeam.organisationId,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
orderBy: {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
folder: true,
|
||||
documentMeta: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
recipients: {
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
fields: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
directLink: {
|
||||
select: {
|
||||
directTemplateRecipientId: true,
|
||||
enabled: true,
|
||||
id: true,
|
||||
token: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Organisation template not found',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...envelope,
|
||||
user: {
|
||||
id: envelope.user.id,
|
||||
name: envelope.user.name || '',
|
||||
email: envelope.user.email,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a where input for querying an organisation template.
|
||||
*
|
||||
* Matches a TEMPLATE envelope with templateType ORGANISATION belonging to any team
|
||||
* within the provided organisation, respecting the caller's team role visibility.
|
||||
*/
|
||||
export const getOrganisationTemplateWhereInput = ({
|
||||
id,
|
||||
organisationId,
|
||||
teamRole,
|
||||
}: {
|
||||
id: EnvelopeIdOptions;
|
||||
organisationId: string;
|
||||
teamRole: TeamMemberRole;
|
||||
}): Prisma.EnvelopeWhereInput => {
|
||||
return {
|
||||
...unsafeBuildEnvelopeIdQuery(id, EnvelopeType.TEMPLATE),
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
templateType: TemplateType.ORGANISATION,
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[teamRole],
|
||||
},
|
||||
team: {
|
||||
organisationId,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -34,19 +34,23 @@ export const searchTemplatesWithKeyword = async ({
|
||||
|
||||
const teamIds = [...teamGroupsByTeamId.keys()];
|
||||
|
||||
const titleOrRecipientMatch: Prisma.EnvelopeWhereInput = {
|
||||
OR: [
|
||||
{ title: { contains: query, mode: 'insensitive' } },
|
||||
{
|
||||
recipients: {
|
||||
some: { email: { contains: query, mode: 'insensitive' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const filters: Prisma.EnvelopeWhereInput[] = [
|
||||
// Templates owned by the user matching title or recipient email.
|
||||
{
|
||||
userId,
|
||||
deletedAt: null,
|
||||
OR: [
|
||||
{ title: { contains: query, mode: 'insensitive' } },
|
||||
{
|
||||
recipients: {
|
||||
some: { email: { contains: query, mode: 'insensitive' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
...titleOrRecipientMatch,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -55,14 +59,7 @@ export const searchTemplatesWithKeyword = async ({
|
||||
filters.push({
|
||||
teamId: { in: teamIds },
|
||||
deletedAt: null,
|
||||
OR: [
|
||||
{ title: { contains: query, mode: 'insensitive' } },
|
||||
{
|
||||
recipients: {
|
||||
some: { email: { contains: query, mode: 'insensitive' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
...titleOrRecipientMatch,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,6 +82,7 @@ export const searchTemplatesWithKeyword = async ({
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
@@ -122,6 +120,7 @@ export const searchTemplatesWithKeyword = async ({
|
||||
.slice(0, limit)
|
||||
.map((envelope) => {
|
||||
const legacyTemplateId = mapSecondaryIdToTemplateId(envelope.secondaryId);
|
||||
|
||||
const path = `${formatTemplatesPath(envelope.team.url)}/${legacyTemplateId}`;
|
||||
|
||||
return {
|
||||
|
||||
@@ -146,6 +146,7 @@ export const ZTemplateManySchema = TemplateSchema.pick({
|
||||
team: TeamSchema.pick({
|
||||
id: true,
|
||||
url: true,
|
||||
name: true,
|
||||
}).nullable(),
|
||||
fields: ZFieldSchema.array(),
|
||||
recipients: ZRecipientLiteSchema.array(),
|
||||
|
||||
Reference in New Issue
Block a user