mirror of
https://github.com/documenso/documenso.git
synced 2025-11-12 15:53:02 +10:00
feat: add external id to documents and templates (#1227)
## Description Adds the external ID column to documents and templates with an option to configure it in the API or UI. External ID's can be used to link a document or template to an external system and identify them via webhooks, etc.
This commit is contained in:
@ -172,6 +172,7 @@ export const EditDocumentForm = ({
|
||||
teamId: team?.id,
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId || null,
|
||||
globalAccessAuth: data.globalAccessAuth ?? null,
|
||||
globalActionAuth: data.globalActionAuth ?? null,
|
||||
},
|
||||
|
||||
@ -132,6 +132,7 @@ export const EditTemplateForm = ({
|
||||
teamId: team?.id,
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId || null,
|
||||
globalAccessAuth: data.globalAccessAuth ?? null,
|
||||
globalActionAuth: data.globalActionAuth ?? null,
|
||||
},
|
||||
|
||||
@ -271,6 +271,23 @@ export const DocumentHistorySheet = ({
|
||||
]}
|
||||
/>
|
||||
))
|
||||
.with(
|
||||
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED },
|
||||
({ data }) => (
|
||||
<DocumentHistorySheetChanges
|
||||
values={[
|
||||
{
|
||||
key: 'Old',
|
||||
value: data.from,
|
||||
},
|
||||
{
|
||||
key: 'New',
|
||||
value: data.to,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
)
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED }, ({ data }) => (
|
||||
<DocumentHistorySheetChanges
|
||||
values={[
|
||||
|
||||
@ -233,6 +233,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
|
||||
const document = await createDocument({
|
||||
title: body.title,
|
||||
externalId: body.externalId || null,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
formValues: body.formValues,
|
||||
@ -398,6 +399,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
teamId: team?.id,
|
||||
data: {
|
||||
title: fileName,
|
||||
externalId: body.externalId || null,
|
||||
formValues: body.formValues,
|
||||
documentData: {
|
||||
connect: {
|
||||
@ -454,6 +456,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
try {
|
||||
document = await createDocumentFromTemplate({
|
||||
templateId,
|
||||
externalId: body.externalId || null,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
recipients: body.recipients,
|
||||
|
||||
@ -29,6 +29,7 @@ export type TDeleteDocumentMutationSchema = typeof ZDeleteDocumentMutationSchema
|
||||
|
||||
export const ZSuccessfulDocumentResponseSchema = z.object({
|
||||
id: z.number(),
|
||||
externalId: z.string().nullish(),
|
||||
userId: z.number(),
|
||||
teamId: z.number().nullish(),
|
||||
title: z.string(),
|
||||
@ -84,6 +85,7 @@ export type TUploadDocumentSuccessfulSchema = z.infer<typeof ZUploadDocumentSucc
|
||||
|
||||
export const ZCreateDocumentMutationSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
externalId: z.string().nullish(),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
name: z.string().min(1),
|
||||
@ -108,6 +110,7 @@ export type TCreateDocumentMutationSchema = z.infer<typeof ZCreateDocumentMutati
|
||||
export const ZCreateDocumentMutationResponseSchema = z.object({
|
||||
uploadUrl: z.string().min(1),
|
||||
documentId: z.number(),
|
||||
externalId: z.string().nullish(),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
recipientId: z.number(),
|
||||
@ -127,6 +130,7 @@ export type TCreateDocumentMutationResponseSchema = z.infer<
|
||||
|
||||
export const ZCreateDocumentFromTemplateMutationSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
externalId: z.string().nullish(),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
name: z.string().min(1),
|
||||
@ -153,6 +157,7 @@ export type TCreateDocumentFromTemplateMutationSchema = z.infer<
|
||||
|
||||
export const ZCreateDocumentFromTemplateMutationResponseSchema = z.object({
|
||||
documentId: z.number(),
|
||||
externalId: z.string().nullish(),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
recipientId: z.number(),
|
||||
@ -172,6 +177,7 @@ export type TCreateDocumentFromTemplateMutationResponseSchema = z.infer<
|
||||
|
||||
export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
|
||||
title: z.string().optional(),
|
||||
externalId: z.string().nullish(),
|
||||
recipients: z
|
||||
.array(
|
||||
z.object({
|
||||
@ -208,6 +214,7 @@ export type TGenerateDocumentFromTemplateMutationSchema = z.infer<
|
||||
|
||||
export const ZGenerateDocumentFromTemplateMutationResponseSchema = z.object({
|
||||
documentId: z.number(),
|
||||
externalId: z.string().nullish(),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
recipientId: z.number(),
|
||||
@ -346,6 +353,7 @@ export const ZTemplateMetaSchema = z.object({
|
||||
|
||||
export const ZTemplateSchema = z.object({
|
||||
id: z.number(),
|
||||
externalId: z.string().nullish(),
|
||||
type: z.nativeEnum(TemplateType),
|
||||
title: z.string(),
|
||||
userId: z.number(),
|
||||
|
||||
@ -11,6 +11,7 @@ import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type CreateDocumentOptions = {
|
||||
title: string;
|
||||
externalId?: string | null;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentDataId: string;
|
||||
@ -21,6 +22,7 @@ export type CreateDocumentOptions = {
|
||||
export const createDocument = async ({
|
||||
userId,
|
||||
title,
|
||||
externalId,
|
||||
documentDataId,
|
||||
teamId,
|
||||
formValues,
|
||||
@ -50,6 +52,7 @@ export const createDocument = async ({
|
||||
const document = await tx.document.create({
|
||||
data: {
|
||||
title,
|
||||
externalId,
|
||||
documentDataId,
|
||||
userId,
|
||||
teamId,
|
||||
|
||||
@ -18,6 +18,7 @@ export type UpdateDocumentSettingsOptions = {
|
||||
documentId: number;
|
||||
data: {
|
||||
title?: string;
|
||||
externalId?: string | null;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
||||
};
|
||||
@ -91,6 +92,7 @@ export const updateDocumentSettings = async ({
|
||||
}
|
||||
|
||||
const isTitleSame = data.title === document.title;
|
||||
const isExternalIdSame = data.externalId === document.externalId;
|
||||
const isGlobalAccessSame = documentGlobalAccessAuth === newGlobalAccessAuth;
|
||||
const isGlobalActionSame = documentGlobalActionAuth === newGlobalActionAuth;
|
||||
|
||||
@ -118,6 +120,21 @@ export const updateDocumentSettings = async ({
|
||||
);
|
||||
}
|
||||
|
||||
if (!isExternalIdSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED,
|
||||
documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
from: document.externalId,
|
||||
to: data.externalId || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isGlobalAccessSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
@ -165,6 +182,7 @@ export const updateDocumentSettings = async ({
|
||||
},
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId || null,
|
||||
authOptions,
|
||||
},
|
||||
});
|
||||
|
||||
@ -33,6 +33,7 @@ export type CreateDocumentFromTemplateResponse = Awaited<
|
||||
|
||||
export type CreateDocumentFromTemplateOptions = {
|
||||
templateId: number;
|
||||
externalId?: string | null;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
recipients: {
|
||||
@ -58,6 +59,7 @@ export type CreateDocumentFromTemplateOptions = {
|
||||
|
||||
export const createDocumentFromTemplate = async ({
|
||||
templateId,
|
||||
externalId,
|
||||
userId,
|
||||
teamId,
|
||||
recipients,
|
||||
@ -147,6 +149,7 @@ export const createDocumentFromTemplate = async ({
|
||||
const document = await tx.document.create({
|
||||
data: {
|
||||
source: DocumentSource.TEMPLATE,
|
||||
externalId,
|
||||
templateId: template.id,
|
||||
userId,
|
||||
teamId: template.teamId,
|
||||
|
||||
@ -15,6 +15,7 @@ export type UpdateTemplateSettingsOptions = {
|
||||
templateId: number;
|
||||
data: {
|
||||
title?: string;
|
||||
externalId?: string | null;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
||||
publicTitle?: string;
|
||||
@ -99,6 +100,7 @@ export const updateTemplateSettings = async ({
|
||||
},
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId || null,
|
||||
type: data.type,
|
||||
publicDescription: data.publicDescription,
|
||||
publicTitle: data.publicTitle,
|
||||
|
||||
@ -35,6 +35,7 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
|
||||
'DOCUMENT_RECIPIENT_COMPLETED', // When a recipient completes all their required tasks for the document.
|
||||
'DOCUMENT_SENT', // When the document transitions from DRAFT to PENDING.
|
||||
'DOCUMENT_TITLE_UPDATED', // When the document title is updated.
|
||||
'DOCUMENT_EXTERNAL_ID_UPDATED', // When the document external ID is updated.
|
||||
'DOCUMENT_MOVED_TO_TEAM', // When the document is moved to a team.
|
||||
]);
|
||||
|
||||
@ -355,6 +356,17 @@ export const ZDocumentAuditLogEventDocumentTitleUpdatedSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Document external ID updated.
|
||||
*/
|
||||
export const ZDocumentAuditLogEventDocumentExternalIdUpdatedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED),
|
||||
data: z.object({
|
||||
from: z.string().nullish(),
|
||||
to: z.string().nullish(),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Field created.
|
||||
*/
|
||||
@ -450,6 +462,7 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
|
||||
ZDocumentAuditLogEventDocumentRecipientCompleteSchema,
|
||||
ZDocumentAuditLogEventDocumentSentSchema,
|
||||
ZDocumentAuditLogEventDocumentTitleUpdatedSchema,
|
||||
ZDocumentAuditLogEventDocumentExternalIdUpdatedSchema,
|
||||
ZDocumentAuditLogEventFieldCreatedSchema,
|
||||
ZDocumentAuditLogEventFieldRemovedSchema,
|
||||
ZDocumentAuditLogEventFieldUpdatedSchema,
|
||||
|
||||
@ -332,6 +332,10 @@ export const formatDocumentAuditLogAction = (auditLog: TDocumentAuditLog, userId
|
||||
anonymous: 'Document title updated',
|
||||
identified: 'updated the document title',
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED }, () => ({
|
||||
anonymous: 'Document external ID updated',
|
||||
identified: 'updated the document external ID',
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT }, () => ({
|
||||
anonymous: 'Document sent',
|
||||
identified: 'sent the document',
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Document" ADD COLUMN "externalId" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Template" ADD COLUMN "externalId" TEXT;
|
||||
@ -283,6 +283,7 @@ enum DocumentSource {
|
||||
|
||||
model Document {
|
||||
id Int @id @default(autoincrement())
|
||||
externalId String?
|
||||
userId Int
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
authOptions Json?
|
||||
@ -589,6 +590,7 @@ model TemplateMeta {
|
||||
|
||||
model Template {
|
||||
id Int @id @default(autoincrement())
|
||||
externalId String?
|
||||
type TemplateType @default(PRIVATE)
|
||||
title String
|
||||
userId Int
|
||||
|
||||
@ -55,6 +55,7 @@ export const ZSetSettingsForDocumentMutationSchema = z.object({
|
||||
teamId: z.number().min(1).optional(),
|
||||
data: z.object({
|
||||
title: z.string().min(1).optional(),
|
||||
externalId: z.string().nullish(),
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(),
|
||||
}),
|
||||
|
||||
@ -74,6 +74,7 @@ export const ZUpdateTemplateSettingsMutationSchema = z.object({
|
||||
teamId: z.number().min(1).optional(),
|
||||
data: z.object({
|
||||
title: z.string().min(1).optional(),
|
||||
externalId: z.string().nullish(),
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(),
|
||||
publicTitle: z.string().trim().min(1).max(MAX_TEMPLATE_PUBLIC_TITLE_LENGTH).optional(),
|
||||
|
||||
@ -78,6 +78,7 @@ export const AddSettingsFormPartial = ({
|
||||
resolver: zodResolver(ZAddSettingsFormSchema),
|
||||
defaultValues: {
|
||||
title: document.title,
|
||||
externalId: document.externalId || '',
|
||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
|
||||
globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
|
||||
meta: {
|
||||
@ -183,6 +184,34 @@ export const AddSettingsFormPartial = ({
|
||||
|
||||
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-2 text-sm leading-relaxed">
|
||||
<div className="flex flex-col space-y-6 ">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="externalId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex flex-row items-center">
|
||||
External ID{' '}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||
Add an external ID to the document. This can be used to identify the
|
||||
document in external systems.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input className="bg-background" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta.dateFormat"
|
||||
|
||||
@ -21,6 +21,7 @@ export const ZMapNegativeOneToUndefinedSchema = z
|
||||
|
||||
export const ZAddSettingsFormSchema = z.object({
|
||||
title: z.string().trim().min(1, { message: "Title can't be empty" }),
|
||||
externalId: z.string().optional(),
|
||||
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZDocumentAccessAuthTypesSchema.optional(),
|
||||
),
|
||||
|
||||
@ -79,6 +79,7 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
resolver: zodResolver(ZAddTemplateSettingsFormSchema),
|
||||
defaultValues: {
|
||||
title: template.title,
|
||||
externalId: template.externalId || undefined,
|
||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
|
||||
globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
|
||||
meta: {
|
||||
@ -223,6 +224,34 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
|
||||
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-4 text-sm leading-relaxed">
|
||||
<div className="flex flex-col space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="externalId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex flex-row items-center">
|
||||
External ID{' '}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||
Add an external ID to the template. This can be used to identify in
|
||||
external systems.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input className="bg-background" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta.dateFormat"
|
||||
|
||||
@ -12,6 +12,7 @@ import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.
|
||||
|
||||
export const ZAddTemplateSettingsFormSchema = z.object({
|
||||
title: z.string().trim().min(1, { message: "Title can't be empty" }),
|
||||
externalId: z.string().optional(),
|
||||
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZDocumentAccessAuthTypesSchema.optional(),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user