diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx index 27c6338062..af3a517f14 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx @@ -102,51 +102,64 @@ import { SelectTrigger, SelectValue, } from '@documenso/ui/primitives/select'; +import { Switch } from '@documenso/ui/primitives/switch'; import { Textarea } from '@documenso/ui/primitives/textarea'; import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { useCurrentTeam } from '~/providers/team'; -export const ZAddSettingsFormSchema = z.object({ - templateType: z.nativeEnum(TemplateType).optional(), - externalId: z.string().optional(), - visibility: z.nativeEnum(DocumentVisibility).optional(), - globalAccessAuth: z - .array(z.union([ZDocumentAccessAuthTypesSchema, z.literal('-1')])) - .transform((val) => (val.length === 1 && val[0] === '-1' ? [] : val)) - .optional() - .default([]), - globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]), - meta: z.object({ - subject: z.string(), - message: z.string(), - timezone: ZDocumentMetaTimezoneSchema.default(DEFAULT_DOCUMENT_TIME_ZONE), - dateFormat: ZDocumentMetaDateFormatSchema.default(DEFAULT_DOCUMENT_DATE_FORMAT), - distributionMethod: z - .nativeEnum(DocumentDistributionMethod) +export const ZAddSettingsFormSchema = z + .object({ + templateType: z.nativeEnum(TemplateType).optional(), + externalId: z.string().optional(), + visibility: z.nativeEnum(DocumentVisibility).optional(), + globalAccessAuth: z + .array(z.union([ZDocumentAccessAuthTypesSchema, z.literal('-1')])) + .transform((val) => (val.length === 1 && val[0] === '-1' ? [] : val)) .optional() - .default(DocumentDistributionMethod.EMAIL), - redirectUrl: z - .string() - .optional() - .refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), { - message: - 'Please enter a valid URL, make sure you include http:// or https:// part of the url.', + .default([]), + globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]), + meta: z.object({ + subject: z.string(), + message: z.string(), + timezone: ZDocumentMetaTimezoneSchema.default(DEFAULT_DOCUMENT_TIME_ZONE), + dateFormat: ZDocumentMetaDateFormatSchema.default(DEFAULT_DOCUMENT_DATE_FORMAT), + distributionMethod: z + .nativeEnum(DocumentDistributionMethod) + .optional() + .default(DocumentDistributionMethod.EMAIL), + redirectUrl: z + .string() + .optional() + .refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), { + message: + 'Please enter a valid URL, make sure you include http:// or https:// part of the url.', + }), + language: z + .union([z.string(), z.enum(SUPPORTED_LANGUAGE_CODES)]) + .optional() + .default('en'), + emailId: z.string().nullable(), + emailReplyTo: z.preprocess((val) => (val === '' ? undefined : val), zEmail().optional()), + emailSettings: ZDocumentEmailSettingsSchema, + signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(1, { + message: msg`At least one signature type must be enabled`.id, }), - language: z - .union([z.string(), z.enum(SUPPORTED_LANGUAGE_CODES)]) - .optional() - .default('en'), - emailId: z.string().nullable(), - emailReplyTo: z.preprocess((val) => (val === '' ? undefined : val), zEmail().optional()), - emailSettings: ZDocumentEmailSettingsSchema, - signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(1, { - message: msg`At least one signature type must be enabled`.id, + envelopeExpirationPeriod: ZEnvelopeExpirationPeriod.nullish(), + reminderEnabled: z.boolean().default(false), + reminderIntervalDays: z.number().int().min(1).max(30).optional(), }), - envelopeExpirationPeriod: ZEnvelopeExpirationPeriod.nullish(), - }), -}); + }) + .superRefine((data, ctx) => { + if (data.meta.reminderEnabled && !data.meta.reminderIntervalDays) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: msg`Reminder interval is required when reminders are enabled`.id, + path: ['meta', 'reminderIntervalDays'], + }); + } + }); type EnvelopeEditorSettingsTabType = 'general' | 'email' | 'security'; @@ -222,6 +235,8 @@ export const EnvelopeEditorSettingsDialog = ({ emailSettings: ZDocumentEmailSettingsSchema.parse(envelope.documentMeta.emailSettings), signatureTypes: extractTeamSignatureSettings(envelope.documentMeta), envelopeExpirationPeriod: envelope.documentMeta?.envelopeExpirationPeriod ?? null, + reminderEnabled: envelope.documentMeta?.reminderEnabled ?? false, + reminderIntervalDays: envelope.documentMeta?.reminderIntervalDays ?? undefined, }, }; }; @@ -239,6 +254,7 @@ export const EnvelopeEditorSettingsDialog = ({ ); const emailSettings = form.watch('meta.emailSettings'); + const envelopeExpirationPeriod = form.watch('meta.envelopeExpirationPeriod'); const { data: emailData, isLoading: isLoadingEmails } = trpc.enterprise.organisation.email.find.useQuery( @@ -270,6 +286,8 @@ export const EnvelopeEditorSettingsDialog = ({ subject, emailReplyTo, envelopeExpirationPeriod, + reminderEnabled, + reminderIntervalDays, } = data.meta; const parsedGlobalAccessAuth = z @@ -300,6 +318,8 @@ export const EnvelopeEditorSettingsDialog = ({ typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE), uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD), envelopeExpirationPeriod, + reminderEnabled, + reminderIntervalDays, }, }); @@ -749,6 +769,82 @@ export const EnvelopeEditorSettingsDialog = ({ )} /> )} + + ( + + + Automatic Reminders + + + + + + + {envelopeExpirationPeriod ? ( + + Automatically send reminder emails to unsigned recipients on a + recurring interval until the document expires. + + ) : ( + + Set an expiration date to enable automatic reminders. + + )} + + + + + + + + + + + )} + /> + + {form.watch('meta.reminderEnabled') && ( + ( + + + Reminder Interval (days) + + + + { + const raw = e.target.value; + if (raw === '') { + field.onChange(undefined); + return; + } + const parsed = parseInt(raw, 10); + field.onChange(isNaN(parsed) ? undefined : parsed); + }} + /> + + + + + )} + /> + )} )) .with( diff --git a/apps/remix/app/routes/embed+/v2+/authoring+/envelope.create._index.tsx b/apps/remix/app/routes/embed+/v2+/authoring+/envelope.create._index.tsx index 57925c5e75..24403cebfe 100644 --- a/apps/remix/app/routes/embed+/v2+/authoring+/envelope.create._index.tsx +++ b/apps/remix/app/routes/embed+/v2+/authoring+/envelope.create._index.tsx @@ -241,6 +241,7 @@ const EnvelopeCreatePage = ({ embedAuthoringOptions }: EnvelopeCreatePageProps) drawSignatureEnabled: envelope.documentMeta.drawSignatureEnabled ?? undefined, dateFormat: (envelope.documentMeta.dateFormat as TDocumentMetaDateFormat) ?? undefined, language: envelope.documentMeta.language as SupportedLanguageCodes, + reminderIntervalDays: envelope.documentMeta.reminderIntervalDays ?? undefined, }, }; diff --git a/apps/remix/app/routes/embed+/v2+/authoring+/envelope.edit.$id.tsx b/apps/remix/app/routes/embed+/v2+/authoring+/envelope.edit.$id.tsx index f2f9f6816b..84cd4bf17d 100644 --- a/apps/remix/app/routes/embed+/v2+/authoring+/envelope.edit.$id.tsx +++ b/apps/remix/app/routes/embed+/v2+/authoring+/envelope.edit.$id.tsx @@ -256,6 +256,7 @@ const EnvelopeEditPage = ({ embedAuthoringOptions }: EnvelopeEditPageProps) => { drawSignatureEnabled: envelope.documentMeta.drawSignatureEnabled, // dateFormat: (envelope.documentMeta.dateFormat as TDocumentMetaDateFormat) ?? undefined, language: envelope.documentMeta.language as SupportedLanguageCodes, + reminderIntervalDays: envelope.documentMeta.reminderIntervalDays ?? undefined, }, }; diff --git a/packages/lib/types/document-email.ts b/packages/lib/types/document-email.ts index 02d9526de5..5d16df782f 100644 --- a/packages/lib/types/document-email.ts +++ b/packages/lib/types/document-email.ts @@ -72,7 +72,7 @@ export const ZDocumentEmailSettingsSchema = z .describe( 'Whether to send a digest email to the document owner when reminders are sent to unsigned recipients. Aggregates all pending documents for the team into a single email.', ) - .default(true), + .default(false), }) .strip() .catch(() => ({ ...DEFAULT_DOCUMENT_EMAIL_SETTINGS })); @@ -115,5 +115,5 @@ export const DEFAULT_DOCUMENT_EMAIL_SETTINGS: TDocumentEmailSettings = { ownerDocumentCompleted: true, ownerRecipientExpired: true, ownerDocumentCreated: true, - ownerReminderDigest: true, + ownerReminderDigest: false, }; diff --git a/packages/lib/types/document-meta.ts b/packages/lib/types/document-meta.ts index f3ad49def1..9817864530 100644 --- a/packages/lib/types/document-meta.ts +++ b/packages/lib/types/document-meta.ts @@ -114,7 +114,7 @@ export const ZDocumentMetaUploadSignatureEnabledSchema = z * all corresponding areas where this is used (some places that use this needs to pass * it through to another function). */ -export const ZDocumentMetaCreateSchema = z.object({ +export const ZDocumentMetaFieldsSchema = z.object({ subject: ZDocumentMetaSubjectSchema.optional(), message: ZDocumentMetaMessageSchema.optional(), timezone: ZDocumentMetaTimezoneSchema.optional(), @@ -131,6 +131,26 @@ export const ZDocumentMetaCreateSchema = z.object({ emailReplyTo: zEmail().nullish(), emailSettings: ZDocumentEmailSettingsSchema.nullish(), envelopeExpirationPeriod: ZEnvelopeExpirationPeriod.nullish(), + reminderEnabled: z.boolean().optional(), + reminderIntervalDays: z.number().int().min(1).max(30).optional(), +}); + +export const ZDocumentMetaCreateSchema = ZDocumentMetaFieldsSchema.superRefine((data, ctx) => { + if (data.reminderEnabled && !data.reminderIntervalDays) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: msg`Reminder interval is required when reminders are enabled`.id, + path: ['reminderIntervalDays'], + }); + } + + if (data.reminderEnabled && !data.envelopeExpirationPeriod) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: msg`An expiration period is required to enable reminders`.id, + path: ['reminderEnabled'], + }); + } }); export type TDocumentMetaCreate = z.infer; diff --git a/packages/lib/types/envelope-editor.ts b/packages/lib/types/envelope-editor.ts index 02cfa4abdd..67f99dc9ce 100644 --- a/packages/lib/types/envelope-editor.ts +++ b/packages/lib/types/envelope-editor.ts @@ -271,6 +271,8 @@ export const ZEditorEnvelopeSchema = EnvelopeSchema.pick({ emailId: true, emailReplyTo: true, envelopeExpirationPeriod: true, + reminderEnabled: true, + reminderIntervalDays: true, }), recipients: ZEnvelopeRecipientLiteSchema.array(), fields: ZEnvelopeFieldSchema.array(), diff --git a/packages/lib/types/envelope.ts b/packages/lib/types/envelope.ts index 806ee97731..391cca9397 100644 --- a/packages/lib/types/envelope.ts +++ b/packages/lib/types/envelope.ts @@ -56,6 +56,8 @@ export const ZEnvelopeSchema = EnvelopeSchema.pick({ emailId: true, emailReplyTo: true, envelopeExpirationPeriod: true, + reminderEnabled: true, + reminderIntervalDays: true, }), recipients: ZEnvelopeRecipientLiteSchema.array(), fields: ZEnvelopeFieldSchema.array(), diff --git a/packages/trpc/server/envelope-router/distribute-envelope.types.ts b/packages/trpc/server/envelope-router/distribute-envelope.types.ts index a43a6fc2d1..e0658fc5d6 100644 --- a/packages/trpc/server/envelope-router/distribute-envelope.types.ts +++ b/packages/trpc/server/envelope-router/distribute-envelope.types.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { ZDocumentMetaUpdateSchema } from '@documenso/lib/types/document-meta'; +import { ZDocumentMetaFieldsSchema } from '@documenso/lib/types/document-meta'; import { ZSuccessResponseSchema } from '../schema'; import type { TrpcRouteMeta } from '../trpc'; @@ -18,7 +18,7 @@ export const distributeEnvelopeMeta: TrpcRouteMeta = { export const ZDistributeEnvelopeRequestSchema = z.object({ envelopeId: z.string().describe('The ID of the envelope to send.'), - meta: ZDocumentMetaUpdateSchema.pick({ + meta: ZDocumentMetaFieldsSchema.pick({ subject: true, message: true, timezone: true, diff --git a/packages/ui/components/document/document-email-checkboxes.tsx b/packages/ui/components/document/document-email-checkboxes.tsx index 200503709e..2dfc8456dd 100644 --- a/packages/ui/components/document/document-email-checkboxes.tsx +++ b/packages/ui/components/document/document-email-checkboxes.tsx @@ -368,6 +368,45 @@ export const DocumentEmailCheckboxes = ({ + +
+ + onChange({ ...value, [DocumentEmailEvents.OwnerReminderDigest]: Boolean(checked) }) + } + /> + + +
); };