Compare commits

..
Author SHA1 Message Date
ephraimduncan a5147acd09 fix: correct attachment setting tests and docs
The E2E email settings checks now include `attachDocument`.
The API docs example sends the full `emailSettings` object,
because each omitted setting becomes `true`.
The admin view and the settings form use the same label for the attachment setting.
The completion email handler keeps the file name code from main.
2026-09-30 22:10:13 +00:00
ephraimduncan b8e6ba1c12 chore: remove translation catalog changes 2026-09-30 21:50:56 +00:00
ephraimduncan 917c698160 chore: remove issue 2766 test changes 2026-09-30 21:03:40 +00:00
ephraimduncan b4f5238206 feat: allow omitting document PDFs from emails 2026-09-30 21:02:42 +00:00
58 changed files with 820 additions and 7595 deletions
@@ -569,6 +569,40 @@ const document = await response.json();
</Tab>
</Tabs>
### Omit document PDFs from email
Set `meta.emailSettings.attachDocument` to `false` when you create or update a document or template. Completion emails still go to the owner and to the enabled recipients, including CC recipients. These emails keep their document links but do not include the PDFs.
| Field | Type | Required | Description |
| ----------------------------------- | ------- | -------- | ------------------------------------------------------------- |
| `meta.emailSettings.attachDocument` | boolean | No | Attach completed document PDFs to emails. Defaults to `true`. |
The `emailSettings` object replaces all saved email settings. Each setting that you leave out becomes `true`. To change only the attachments, send the complete object with your current values:
```json
{
"envelopeId": "envelope_abc123",
"meta": {
"emailSettings": {
"attachDocument": false,
"recipientSigningRequest": true,
"recipientRemoved": true,
"recipientSigned": true,
"documentPending": true,
"documentCompleted": true,
"documentDeleted": true,
"ownerDocumentCompleted": true,
"ownerRecipientExpired": true,
"ownerDocumentCreated": true
}
}
}
```
When you use a template with `/envelope/use`, set the option in `override.emailSettings`. The document and template APIs, including API v1, accept the same option. The API rejects values that are not booleans.
If you omit the email settings on creation, the document inherits the team's defaults, which can inherit from the organisation. Existing documents and templates keep their saved settings when those defaults change.
---
## Send Document
@@ -149,7 +149,6 @@ Fields expose data attributes for state-based styling:
| `[data-field-type]` | `SIGNATURE`, `TEXT`, `CHECKBOX`, `RADIO`, etc. | The type of field |
| `[data-inserted]` | `true`, `false` | Whether the field has been filled |
| `[data-validate]` | `true`, `false` | Whether the field is being validated |
| `[data-readonly]` | `true`, `false` | Whether the field is read-only |
### Example
@@ -161,7 +160,7 @@ Fields expose data attributes for state-based styling:
/* Style filled fields */
.field--FieldRootContainer[data-inserted='true'] {
background-color: hsl(var(--primary));
background-color: var(--primary);
opacity: 0.2;
}
@@ -191,6 +191,9 @@ For manual deployments or troubleshooting:
```bash
# Apply pending migrations
npm run prisma:migrate-deploy
# Or using npx directly
npx prisma migrate deploy
```
<Callout type="info">
@@ -426,9 +426,8 @@ docker pull documenso/documenso:<version>
# Run migrations only
docker run --rm \
-e NEXT_PRIVATE_DATABASE_URL="postgresql://user:password@host:5432/documenso" \
-e NEXT_PRIVATE_DIRECT_DATABASE_URL="postgresql://user:password@host:5432/documenso" \
documenso/documenso:<version> \
npx prisma migrate deploy --schema ../../packages/prisma/schema.prisma
npx prisma migrate deploy
```
<Callout type="warn">
@@ -48,6 +48,12 @@ Or you can customize it before sending the document, by clicking on the **Docume
![Editor email settings](/document-signing/documenso-editor-email-settings.webp)
### Document PDF attachments
In **Document Settings > Notifications**, clear **Attach completed document PDFs to emails** to send completion emails with document links only. This applies to the owner and all recipients, including CCs. The email notifications remain enabled, and recipients can access the document through their links.
PDF attachments are enabled by default. Signing invitations and pending emails already include links without document PDFs. You can also set the attachment default for new documents and templates in your [email preferences](/docs/users/organisations/preferences/email).
### Reply-to email (optional)
Set a reply-to address so recipient replies go to a specific email instead of the default.
@@ -25,6 +25,12 @@ To access the preferences, navigate to either the organisation or teams settings
| **Reply to email** | Email address used in the "Reply To" field in outgoing emails. Leave blank to inherit from the organisation. |
| **Default Email Settings** | Controls which emails are sent to recipients during document signing (e.g. signing invitations, completion notifications). Teams can inherit from the organisation or override with custom settings. |
### Document PDF attachments
Under **Default Email Settings**, clear **Attach completed document PDFs to emails** to omit PDFs from completion emails to the owner and all recipients, including CCs. Emails still include document links. Attachments are enabled by default.
Teams inherit this preference with the other email settings. Select **Override organisation settings** to configure it for a team, or **Inherit from organisation** to restore inheritance. Changes apply to new documents and templates; existing documents and templates keep their saved settings. You can change the attachment setting for an individual document or template in **Document Settings > Notifications**.
## See Also
- [Email Domains](/docs/users/organisations/email-domains) - Send emails from your own domain
+22 -41
View File
@@ -1,11 +1,6 @@
import communityCardsImage from '@documenso/assets/images/community-cards.png';
import { authClient } from '@documenso/auth/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import {
NEXT_PUBLIC_IMPRINT_URL,
NEXT_PUBLIC_PRIVACY_POLICY_URL,
NEXT_PUBLIC_TERMS_OF_SERVICE_URL,
} from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { ZNameSchema } from '@documenso/lib/types/name';
import { env } from '@documenso/lib/utils/env';
@@ -25,8 +20,7 @@ import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { TurnstileInstance } from '@marsidev/react-turnstile';
import { Turnstile } from '@marsidev/react-turnstile';
import type { ReactNode } from 'react';
import { Fragment, useEffect, useRef } from 'react';
import { useEffect, useRef } from 'react';
import { useForm } from 'react-hook-form';
import { FaIdCardClip } from 'react-icons/fa6';
import { FcGoogle } from 'react-icons/fc';
@@ -108,14 +102,6 @@ export const SignUpForm = ({
const isSubmitting = form.formState.isSubmitting;
const legalLinks: { href: string | undefined; label: ReactNode }[] = [
{ href: NEXT_PUBLIC_TERMS_OF_SERVICE_URL(), label: <Trans>Terms of Service</Trans> },
{ href: NEXT_PUBLIC_PRIVACY_POLICY_URL(), label: <Trans>Privacy Policy</Trans> },
{ href: NEXT_PUBLIC_IMPRINT_URL(), label: <Trans>Imprint</Trans> },
];
const availableLegalLinks = legalLinks.filter((link): link is { href: string; label: ReactNode } => !!link.href);
const onFormSubmit = async ({ name, email, password, signature }: TSignUpFormSchema) => {
try {
let token: string | undefined;
@@ -420,32 +406,27 @@ export const SignUpForm = ({
)}
</form>
</Form>
{availableLegalLinks.length > 0 && (
<p className="mt-6 text-muted-foreground text-xs">
<Trans>
By proceeding, you agree to our{' '}
{availableLegalLinks.map((link, index) => (
<Fragment key={link.href}>
{index > 0 && (
<>
{' '}
<Trans>and</Trans>{' '}
</>
)}
<Link
to={link.href}
target="_blank"
rel="noopener noreferrer"
className="text-documenso-700 duration-200 hover:opacity-70"
>
{link.label}
</Link>
</Fragment>
))}
.
</Trans>
</p>
)}
<p className="mt-6 text-muted-foreground text-xs">
<Trans>
By proceeding, you agree to our{' '}
<Link
to="https://documen.so/terms"
target="_blank"
className="text-documenso-700 duration-200 hover:opacity-70"
>
Terms of Service
</Link>{' '}
and{' '}
<Link
to="https://documen.so/privacy"
target="_blank"
className="text-documenso-700 duration-200 hover:opacity-70"
>
Privacy Policy
</Link>
.
</Trans>
</p>
</div>
</div>
);
@@ -10,6 +10,7 @@ import type { ReactNode } from 'react';
import { DetailsCard, DetailsValue } from '~/components/general/admin-details';
const EMAIL_SETTINGS_LABELS: Record<keyof TDocumentEmailSettings, MessageDescriptor> = {
attachDocument: msg`Attach completed document PDFs to emails`,
recipientSigningRequest: msg`Recipient signing request`,
recipientRemoved: msg`Recipient removed`,
recipientSigned: msg`Recipient signed`,
@@ -597,11 +597,7 @@ export const AppCommandMenu = ({ open, onOpenChange }: AppCommandMenuProps) => {
<span className="ml-auto text-muted-foreground text-xs">
{hasValidSearch ? (
isVisibleCountCapped ? (
<Plural
value={totalVisibleCount}
one="≥# result"
other="≥# results"
/>
<Trans>{formatChipCount(totalVisibleCount, isVisibleCountCapped)} results</Trans>
) : (
<Plural value={totalVisibleCount} one="# result" other="# results" />
)
@@ -17,7 +17,6 @@ import { match } from 'ts-pattern';
import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog';
import { useEmbedSigningContext } from '~/components/embed/embed-signing-context';
import { BrandingLogo } from '~/components/general/branding-logo';
import { PublicLegalLinks } from '~/components/general/public-legal-links';
import { BrandingLogoIcon } from '../branding-logo-icon';
import { DocumentSigningRejectDialog } from '../document-signing/document-signing-reject-dialog';
@@ -69,10 +68,8 @@ export const EnvelopeSignerHeader = () => {
</div>
{/* Right side - Desktop content */}
<div className="hidden items-center space-x-4 lg:flex">
<PublicLegalLinks />
<p className="flex-shrink-0 text-muted-foreground text-sm">
<div className="hidden items-center space-x-2 lg:flex">
<p className="mr-2 flex-shrink-0 text-muted-foreground text-sm">
<Plural one="1 Field Remaining" other="# Fields Remaining" value={recipientFieldsRemaining.length} />
</p>
@@ -1,60 +0,0 @@
import {
NEXT_PUBLIC_IMPRINT_URL,
NEXT_PUBLIC_PRIVACY_POLICY_URL,
NEXT_PUBLIC_TERMS_OF_SERVICE_URL,
} from '@documenso/lib/constants/app';
import { cn } from '@documenso/ui/lib/utils';
import { Trans } from '@lingui/react/macro';
import { Link } from 'react-router';
export type PublicLegalLinksProps = {
className?: string;
};
export const PublicLegalLinks = ({ className }: PublicLegalLinksProps) => {
const termsUrl = NEXT_PUBLIC_TERMS_OF_SERVICE_URL();
const privacyUrl = NEXT_PUBLIC_PRIVACY_POLICY_URL();
const imprintUrl = NEXT_PUBLIC_IMPRINT_URL();
return (
<div
className={cn(
'flex flex-wrap items-center justify-center gap-x-4 gap-y-2 text-xs text-muted-foreground',
className,
)}
>
{termsUrl && (
<Link
to={termsUrl}
target="_blank"
rel="noopener noreferrer"
className="duration-200 hover:text-foreground hover:underline"
>
<Trans>Terms of Service</Trans>
</Link>
)}
{privacyUrl && (
<Link
to={privacyUrl}
target="_blank"
rel="noopener noreferrer"
className="duration-200 hover:text-foreground hover:underline"
>
<Trans>Privacy Policy</Trans>
</Link>
)}
{imprintUrl && (
<Link
to={imprintUrl}
target="_blank"
rel="noopener noreferrer"
className="duration-200 hover:text-foreground hover:underline"
>
<Trans>Imprint</Trans>
</Link>
)}
</div>
);
};
@@ -12,7 +12,7 @@ import { ChevronDownIcon, ChevronsUpDownIcon, ChevronUpIcon } from 'lucide-react
import { useMemo } from 'react';
import { Link, useSearchParams } from 'react-router';
type OrderByColumn = 'documentCount' | 'emailCount' | 'apiCount' | 'emailReports' | 'teamCount' | 'totalCount';
type OrderByColumn = 'documentCount' | 'emailCount' | 'apiCount' | 'emailReports' | 'totalCount';
type OrderByDirection = 'asc' | 'desc';
const parseOrderByColumn = (value: string | undefined): OrderByColumn | undefined => {
@@ -21,7 +21,6 @@ const parseOrderByColumn = (value: string | undefined): OrderByColumn | undefine
value === 'emailCount' ||
value === 'apiCount' ||
value === 'emailReports' ||
value === 'teamCount' ||
value === 'totalCount'
) {
return value;
@@ -179,11 +178,6 @@ export const AdminOrganisationStatsTable = ({ displayMode = 'usage' }: AdminOrga
accessorKey: 'originalClaimId',
cell: ({ row }) => <span className="text-muted-foreground text-sm">{row.original.originalClaimId ?? '—'}</span>,
},
{
header: () => sortableHeader(t`Teams`, 'teamCount'),
accessorKey: 'teamCount',
cell: ({ row }) => row.original.teamCount,
},
{
header: t`Period`,
accessorKey: 'period',
@@ -246,9 +240,6 @@ export const AdminOrganisationStatsTable = ({ displayMode = 'usage' }: AdminOrga
<TableCell>
<Skeleton className="h-4 w-24 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-10 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-16 rounded-full" />
</TableCell>
@@ -37,7 +37,7 @@ import { useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { Link, useNavigate } from 'react-router';
import { match } from 'ts-pattern';
import { z } from 'zod';
import type { z } from 'zod';
import { AdminOrganisationDeleteDialog } from '~/components/dialogs/admin-organisation-delete-dialog';
import { AdminOrganisationMemberDeleteDialog } from '~/components/dialogs/admin-organisation-member-delete-dialog';
@@ -563,15 +563,11 @@ const GenericOrganisationAdminForm = ({ organisation }: OrganisationAdminFormOpt
);
};
const ZUpdateOrganisationBillingFormSchema = ZUpdateAdminOrganisationRequestSchema.shape.data
.pick({
claims: true,
customerId: true,
originalSubscriptionClaimId: true,
})
.extend({
notifyOrganisation: z.boolean(),
});
const ZUpdateOrganisationBillingFormSchema = ZUpdateAdminOrganisationRequestSchema.shape.data.pick({
claims: true,
customerId: true,
originalSubscriptionClaimId: true,
});
type TUpdateOrganisationBillingFormSchema = z.infer<typeof ZUpdateOrganisationBillingFormSchema>;
@@ -618,31 +614,19 @@ const OrganisationAdminForm = ({ organisation, licenseFlags }: OrganisationAdmin
emailTransportId: organisation.organisationClaim.emailTransportId ?? null,
},
originalSubscriptionClaimId: organisation.organisationClaim.originalSubscriptionClaimId || '',
notifyOrganisation: true,
},
});
const onSubmit = async ({ notifyOrganisation, ...values }: TUpdateOrganisationBillingFormSchema) => {
const onSubmit = async (values: TUpdateOrganisationBillingFormSchema) => {
try {
const { isNotificationSent } = await updateOrganisation({
await updateOrganisation({
organisationId: organisation.id,
data: values,
notifyOrganisation,
});
form.setValue('notifyOrganisation', false);
const description = match({ notifyOrganisation, isNotificationSent })
.with({ isNotificationSent: true }, () => t`Organisation has been updated and its owner has been notified`)
.with(
{ notifyOrganisation: true },
() => t`Organisation has been updated. No limits changed, so no notification was sent`,
)
.otherwise(() => t`Organisation has been updated successfully`);
toast({
title: t`Success`,
description,
description: t`Organisation has been updated successfully`,
duration: 5000,
});
} catch (err) {
@@ -935,31 +919,6 @@ const OrganisationAdminForm = ({ organisation, licenseFlags }: OrganisationAdmin
)}
/>
<FormField
control={form.control}
name="notifyOrganisation"
render={({ field }) => (
<FormItem>
<div className="flex items-center">
<FormControl>
<Checkbox id="notify-organisation" checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
<label className="ml-2 text-muted-foreground text-sm" htmlFor="notify-organisation">
<Trans>Notify the organisation about limit changes</Trans>
</label>
</div>
<FormDescription>
<Trans>
Emails the organisation owner to let them know their limits have been updated. No email is sent if no
limits changed.
</Trans>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit" loading={form.formState.isSubmitting}>
<Trans>Update</Trans>
@@ -35,7 +35,7 @@ export default function OrganisationSettingsGeneral() {
emailId,
emailReplyTo: emailReplyTo || null,
// emailReplyToName,
emailDocumentSettings,
emailDocumentSettings: emailDocumentSettings ?? undefined,
includeSenderDetails: includeSenderDetails ?? undefined,
},
});
+9 -19
View File
@@ -10,8 +10,6 @@ import { useEffect } from 'react';
import { isRouteErrorResponse, Link, Outlet } from 'react-router';
import { Header as AuthenticatedHeader } from '~/components/general/app-header';
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
import { PublicLegalLinks } from '~/components/general/public-legal-links';
import type { Route } from './+types/_layout';
export function meta() {
@@ -37,24 +35,16 @@ export default function RecipientLayout({ matches }: Route.ComponentProps) {
);
return (
<div className="flex min-h-screen flex-col justify-between">
<div>
{!hideHeader && sessionData?.user && <AuthenticatedHeader />}
<div className="min-h-screen">
{!hideHeader && sessionData?.user && <AuthenticatedHeader />}
<main
className={cn({
'mt-8 mb-8 px-4 md:mt-12 md:mb-12 md:px-8': !hideHeader,
})}
>
<Outlet />
</main>
</div>
{!hideHeader && (
<footer className="py-4">
<PublicLegalLinks />
</footer>
)}
<main
className={cn({
'mt-8 mb-8 px-4 md:mt-12 md:mb-12 md:px-8': !hideHeader,
})}
>
<Outlet />
</main>
</div>
);
}
@@ -1,12 +1,10 @@
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
import { Outlet } from 'react-router';
import { PublicLegalLinks } from '~/components/general/public-legal-links';
export default function Layout() {
return (
<main className="relative flex min-h-screen flex-col items-center justify-between overflow-hidden px-4 py-12 md:p-12 lg:p-24">
<div className="flex w-full flex-1 flex-col items-center justify-center">
<main className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-24">
<div>
<div className="absolute -inset-[min(600px,max(400px,60vw))] -z-[1] flex items-center justify-center opacity-70">
<img
src={backgroundPattern}
@@ -19,14 +17,10 @@ export default function Layout() {
/>
</div>
<div className="relative w-full flex items-center justify-center">
<div className="relative w-full">
<Outlet />
</div>
</div>
<footer className="z-10 mt-8 w-full">
<PublicLegalLinks />
</footer>
</main>
);
}
@@ -54,7 +54,7 @@ export default function SignUp({ loaderData }: Route.ComponentProps) {
return (
<SignUpForm
className="w-screen max-w-screen-2xl px-4 md:px-16 lg:-mt-16"
className="w-screen max-w-screen-2xl px-4 md:px-16 lg:-my-16"
isEmailPasswordSignupEnabled={isEmailPasswordSignupEnabled}
isGoogleSignupEnabled={isGoogleSignupEnabled}
isMicrosoftSignupEnabled={isMicrosoftSignupEnabled}
-3
View File
@@ -24,9 +24,6 @@ printf "🏥 Health check: http://localhost:3000/api/health\n"
printf "📊 Certificate status: http://localhost:3000/api/certificate-status\n"
printf "👥 Community: https://github.com/documenso/documenso\n\n"
# The Prisma schema requires a direct URL. Without a connection pooler, it is the same as the database URL.
export NEXT_PRIVATE_DIRECT_DATABASE_URL="${NEXT_PRIVATE_DIRECT_DATABASE_URL:-$NEXT_PRIVATE_DATABASE_URL}"
printf "🗄️ Running database migrations...\n"
npx prisma migrate deploy --schema ../../packages/prisma/schema.prisma
+3 -3
View File
@@ -8,7 +8,7 @@ import {
ZDocumentActionAuthTypesSchema,
ZRecipientActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email';
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
import { ZFieldMetaPrefillFieldsSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { ZRecipientSigningOrderSchema } from '@documenso/lib/types/recipient';
@@ -172,7 +172,7 @@ export const ZCreateDocumentMutationSchema = z.object({
uploadSignatureEnabled: z.boolean().optional().default(true),
drawSignatureEnabled: z.boolean().optional().default(true),
distributionMethod: z.nativeEnum(DocumentDistributionMethod).optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
emailSettings: ZDocumentEmailSettingsInputSchema.optional(),
})
.partial()
.optional()
@@ -341,7 +341,7 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
typedSignatureEnabled: z.boolean(),
uploadSignatureEnabled: z.boolean(),
drawSignatureEnabled: z.boolean(),
emailSettings: ZDocumentEmailSettingsSchema,
emailSettings: ZDocumentEmailSettingsInputSchema,
})
.partial()
.optional(),
@@ -198,6 +198,7 @@ test.describe('API V2 Envelopes', () => {
drawSignatureEnabled: false,
emailReplyTo: userA.email,
emailSettings: {
attachDocument: false,
recipientSigningRequest: false,
recipientRemoved: false,
recipientSigned: false,
@@ -230,6 +230,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
// Check that the team settings have inherited these values.
expect(teamSettings.emailReplyTo).toEqual('organisation@documenso.com');
expect(teamSettings.emailDocumentSettings).toEqual({
attachDocument: true,
recipientSigningRequest: true,
recipientRemoved: true,
recipientSigned: false, // unchecked
@@ -273,6 +274,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
// Check that the team settings have overridden the organisation values.
expect(updatedTeamSettings.emailReplyTo).toEqual('team@example.com');
expect(updatedTeamSettings.emailDocumentSettings).toEqual({
attachDocument: true,
recipientSigned: true,
recipientSigningRequest: false,
recipientRemoved: true,
@@ -295,6 +297,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
expect(teamOverrideDocumentMeta.emailReplyTo).toEqual('team@example.com');
expect(teamOverrideDocumentMeta.emailSettings).toEqual({
attachDocument: true,
recipientSigned: true,
recipientSigningRequest: false,
recipientRemoved: true,
@@ -322,6 +325,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
// Check that the team settings now inherit from organisation again.
expect(inheritedTeamSettings.emailReplyTo).toEqual('organisation@documenso.com');
expect(inheritedTeamSettings.emailDocumentSettings).toEqual({
attachDocument: true,
recipientSigningRequest: true,
recipientRemoved: true,
recipientSigned: false,
@@ -344,6 +348,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
expect(documentMeta.emailReplyTo).toEqual('organisation@documenso.com');
expect(documentMeta.emailSettings).toEqual({
attachDocument: true,
recipientSigningRequest: true,
recipientRemoved: true,
recipientSigned: false,
@@ -1,70 +0,0 @@
import { SUPPORT_EMAIL } from '@documenso/lib/constants/app';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { Body, Container, Head, Hr, Html, Preview, Section, Text } from '../components';
import { TemplateBrandingLogo } from '../template-components/template-branding-logo';
import { TemplateFooter } from '../template-components/template-footer';
export type OrganisationLimitsUpdatedEmailProps = {
assetBaseUrl: string;
organisationName: string;
};
export const OrganisationLimitsUpdatedEmailTemplate = ({
assetBaseUrl = 'http://localhost:3002',
organisationName = 'Organisation Name',
}: OrganisationLimitsUpdatedEmailProps) => {
const { _ } = useLingui();
return (
<Html>
<Head />
<Body className="mx-auto my-auto font-sans">
<Preview>{_(ORGANISATION_LIMITS_UPDATED_SUBJECT)}</Preview>
<Section className="bg-background text-muted-foreground">
<Container className="mx-auto mt-8 mb-2 max-w-xl rounded-lg border border-border border-solid p-2 backdrop-blur-sm">
<TemplateBrandingLogo assetBaseUrl={assetBaseUrl} className="mb-4 h-6 p-2" />
<Section className="p-2 text-muted-foreground">
<Text className="text-center font-medium text-foreground text-lg">
{_(ORGANISATION_LIMITS_UPDATED_SUBJECT)}
</Text>
<div className="mx-auto my-2 w-fit rounded-lg bg-muted px-4 py-2 font-medium text-base text-muted-foreground">
{organisationName}
</div>
<Text className="text-center text-base">
<Trans>We've updated the usage limits for your organisation.</Trans>
</Text>
<Text className="text-center text-base">
<Trans>
If you recently received an email regarding your usage limits, this change was made in response to
that.
</Trans>
</Text>
<Text className="text-center text-base">
<Trans>If you have any questions, please contact support at {SUPPORT_EMAIL}.</Trans>
</Text>
</Section>
</Container>
<Hr className="mx-auto mt-12 max-w-xl" />
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Html>
);
};
export const ORGANISATION_LIMITS_UPDATED_SUBJECT = msg`Your organisation limits have been updated`;
export default OrganisationLimitsUpdatedEmailTemplate;
-8
View File
@@ -45,14 +45,6 @@ export const formatPath = (path: string): string => {
export const NEXT_PUBLIC_SIGNING_CONTACT_INFO = () =>
env('NEXT_PUBLIC_SIGNING_CONTACT_INFO') ?? NEXT_PUBLIC_WEBAPP_URL();
export const NEXT_PUBLIC_TERMS_OF_SERVICE_URL = () =>
env('NEXT_PUBLIC_TERMS_OF_SERVICE_URL') ?? (IS_DOCUMENSO_CLOUD() ? 'https://documen.so/terms' : undefined);
export const NEXT_PUBLIC_PRIVACY_POLICY_URL = () =>
env('NEXT_PUBLIC_PRIVACY_POLICY_URL') ?? (IS_DOCUMENSO_CLOUD() ? 'https://documen.so/privacy' : undefined);
export const NEXT_PUBLIC_IMPRINT_URL = () => env('NEXT_PUBLIC_IMPRINT_URL');
export const NEXT_PRIVATE_USE_LEGACY_SIGNING_SUBFILTER = () =>
env('NEXT_PRIVATE_USE_LEGACY_SIGNING_SUBFILTER') === 'true';
-2
View File
@@ -7,7 +7,6 @@ import { SEND_DOCUMENT_CREATED_FROM_DIRECT_TEMPLATE_EMAIL_JOB_DEFINITION } from
import { SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-deleted-emails';
import { SEND_DOCUMENT_PENDING_EMAIL_JOB_DEFINITION } from './definitions/emails/send-document-pending-email';
import { SEND_ORGANISATION_LIMIT_ALERT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-limit-alert-email';
import { SEND_ORGANISATION_LIMITS_UPDATED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-limits-updated-email';
import { SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-member-joined-email';
import { SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-member-left-email';
import { SEND_OWNER_RECIPIENT_EXPIRED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-owner-recipient-expired-email';
@@ -44,7 +43,6 @@ export const jobsClient = new JobClient([
SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION,
SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION,
SEND_ORGANISATION_LIMIT_ALERT_EMAIL_JOB_DEFINITION,
SEND_ORGANISATION_LIMITS_UPDATED_EMAIL_JOB_DEFINITION,
SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION,
SEAL_DOCUMENT_JOB_DEFINITION,
SEAL_DOCUMENT_SWEEP_JOB_DEFINITION,
@@ -26,17 +26,6 @@ export const run = async ({ payload, io }: { payload: TSendDocumentCompletedEmai
const envelope = await prisma.envelope.findUnique({
where: unsafeBuildEnvelopeIdQuery({ type: 'envelopeId', id: envelopeId }, EnvelopeType.DOCUMENT),
include: {
envelopeItems: {
include: {
documentData: {
select: {
type: true,
id: true,
data: true,
},
},
},
},
documentMeta: true,
recipients: true,
user: {
@@ -83,8 +72,32 @@ export const run = async ({ payload, io }: { payload: TSendDocumentCompletedEmai
const { user: owner } = envelope;
const emailSettings = extractDerivedDocumentEmailSettings(envelope.documentMeta);
const isDocumentCompletedEmailEnabled = emailSettings.documentCompleted;
const isOwnerDocumentCompletedEmailEnabled = emailSettings.ownerDocumentCompleted;
if (!isDocumentCompletedEmailEnabled && !isOwnerDocumentCompletedEmailEnabled) {
return;
}
// Fetch PDF data only when attachments are enabled. Database-backed PDFs can
// contain the entire file in documentData.data.
const envelopeItems = emailSettings.attachDocument
? await prisma.envelopeItem.findMany({
where: { envelopeId: envelope.id },
include: {
documentData: {
select: {
type: true,
data: true,
},
},
},
})
: [];
const completedDocumentEmailAttachments = await Promise.all(
envelope.envelopeItems.map(async (envelopeItem) => {
envelopeItems.map(async (envelopeItem) => {
const file = await getFileServerSide(envelopeItem.documentData);
// Use the envelope title for version 1, and the envelope item title for version 2.
@@ -108,10 +121,6 @@ export const run = async ({ payload, io }: { payload: TSendDocumentCompletedEmai
documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/t/${envelope.team.url}/documents/${envelope.id}`;
}
const emailSettings = extractDerivedDocumentEmailSettings(envelope.documentMeta);
const isDocumentCompletedEmailEnabled = emailSettings.documentCompleted;
const isOwnerDocumentCompletedEmailEnabled = emailSettings.ownerDocumentCompleted;
// Send email to document owner if:
// 1. Owner document completed emails are enabled AND
// 2. Either:
@@ -129,7 +129,6 @@ export const run = async ({
text: `
Organisation: ${organisation.name}
Organisation ID: ${organisation.id}
Admin URL: ${NEXT_PUBLIC_WEBAPP_URL()}/admin/organisations/${organisation.id}
Organisation Claim Original ID: ${organisation.organisationClaim.originalSubscriptionClaimId}
Email Quota: ${organisation.monthlyStats[0]?.emailCount || 0}/${organisation.organisationClaim.emailQuota ?? 'Unlimited'}
API Quota: ${organisation.monthlyStats[0]?.apiCount || 0}/${organisation.organisationClaim.apiQuota ?? 'Unlimited'}
@@ -1,65 +0,0 @@
import OrganisationLimitsUpdatedEmailTemplate, {
ORGANISATION_LIMITS_UPDATED_SUBJECT,
} from '@documenso/email/templates/organisation-limits-updated';
import { prisma } from '@documenso/prisma';
import { createElement } from 'react';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
import { getEmailContext } from '../../../server-only/email/get-email-context';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSendOrganisationLimitsUpdatedEmailJobDefinition } from './send-organisation-limits-updated-email';
export const run = async ({
payload,
io,
}: {
payload: TSendOrganisationLimitsUpdatedEmailJobDefinition;
io: JobRunIO;
}) => {
const organisation = await prisma.organisation.findFirstOrThrow({
where: {
id: payload.organisationId,
},
select: {
id: true,
name: true,
owner: {
select: {
email: true,
},
},
},
});
const { branding, emailLanguage, senderEmail, emailTransport } = await getEmailContext({
emailType: 'INTERNAL',
source: {
type: 'organisation',
organisationId: organisation.id,
},
});
await io.runTask('send-organisation-limits-updated-email', async () => {
const emailContent = createElement(OrganisationLimitsUpdatedEmailTemplate, {
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
organisationName: organisation.name,
});
const [html, text] = await Promise.all([
renderEmailWithI18N(emailContent, { lang: emailLanguage, branding }),
renderEmailWithI18N(emailContent, { lang: emailLanguage, branding, plainText: true }),
]);
const i18n = await getI18nInstance(emailLanguage);
await emailTransport.sendMail({
to: organisation.owner.email,
from: senderEmail,
subject: i18n._(ORGANISATION_LIMITS_UPDATED_SUBJECT),
html,
text,
});
});
};
@@ -1,31 +0,0 @@
import { z } from 'zod';
import type { JobDefinition } from '../../client/_internal/job';
const SEND_ORGANISATION_LIMITS_UPDATED_EMAIL_JOB_DEFINITION_ID = 'send.organisation-limits-updated.email';
const SEND_ORGANISATION_LIMITS_UPDATED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
organisationId: z.string(),
});
export type TSendOrganisationLimitsUpdatedEmailJobDefinition = z.infer<
typeof SEND_ORGANISATION_LIMITS_UPDATED_EMAIL_JOB_DEFINITION_SCHEMA
>;
export const SEND_ORGANISATION_LIMITS_UPDATED_EMAIL_JOB_DEFINITION = {
id: SEND_ORGANISATION_LIMITS_UPDATED_EMAIL_JOB_DEFINITION_ID,
name: 'Send Organisation Limits Updated Email',
version: '1.0.0',
trigger: {
name: SEND_ORGANISATION_LIMITS_UPDATED_EMAIL_JOB_DEFINITION_ID,
schema: SEND_ORGANISATION_LIMITS_UPDATED_EMAIL_JOB_DEFINITION_SCHEMA,
},
handler: async ({ payload, io }) => {
const handler = await import('./send-organisation-limits-updated-email.handler');
await handler.run({ payload, io });
},
} as const satisfies JobDefinition<
typeof SEND_ORGANISATION_LIMITS_UPDATED_EMAIL_JOB_DEFINITION_ID,
TSendOrganisationLimitsUpdatedEmailJobDefinition
>;
@@ -437,6 +437,7 @@ export const generateSampleWebhookPayload = (event: WebhookTriggerEvents, webhoo
dateFormat: 'yyyy-MM-dd hh:mm a',
redirectUrl: '',
emailSettings: {
attachDocument: true,
documentDeleted: true,
documentPending: true,
recipientSigned: true,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+16 -3
View File
@@ -14,8 +14,14 @@ export enum DocumentEmailEvents {
OwnerDocumentCreated = 'ownerDocumentCreated',
}
export const ZDocumentEmailSettingsSchema = z
export const ZDocumentEmailSettingsInputSchema = z
.object({
attachDocument: z
.boolean()
.describe(
'Whether to attach the completed document PDFs to emails sent to the owner and recipients. Document links are always included.',
)
.default(true),
recipientSigningRequest: z
.boolean()
.describe('Whether to send an email to all recipients that the document is ready for them to sign.')
@@ -55,8 +61,13 @@ export const ZDocumentEmailSettingsSchema = z
.describe('Whether to send an email to the document owner when a document is created from a direct template.')
.default(true),
})
.strip()
.catch(() => ({ ...DEFAULT_DOCUMENT_EMAIL_SETTINGS }));
.strip();
// Persisted settings may be absent or predate new options. API inputs must use
// the input schema so invalid values cannot silently enable document attachments.
export const ZDocumentEmailSettingsSchema = ZDocumentEmailSettingsInputSchema.catch(() => ({
...DEFAULT_DOCUMENT_EMAIL_SETTINGS,
}));
export type TDocumentEmailSettings = z.infer<typeof ZDocumentEmailSettingsSchema>;
@@ -68,6 +79,7 @@ export const extractDerivedDocumentEmailSettings = (documentMeta?: DocumentMeta
}
return {
attachDocument: emailSettings.attachDocument,
recipientSigningRequest: false,
recipientRemoved: false,
recipientSigned: false,
@@ -81,6 +93,7 @@ export const extractDerivedDocumentEmailSettings = (documentMeta?: DocumentMeta
};
export const DEFAULT_DOCUMENT_EMAIL_SETTINGS: TDocumentEmailSettings = {
attachDocument: true,
recipientSigningRequest: true,
recipientRemoved: true,
recipientSigned: true,
+2 -2
View File
@@ -9,7 +9,7 @@ import { msg } from '@lingui/core/macro';
import { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/client';
import { z } from 'zod';
import { ZDocumentEmailSettingsSchema } from './document-email';
import { ZDocumentEmailSettingsInputSchema } from './document-email';
/**
* The full document response schema.
@@ -125,7 +125,7 @@ export const ZDocumentMetaCreateSchema = z.object({
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
emailId: z.string().nullish(),
emailReplyTo: zEmail().nullish(),
emailSettings: ZDocumentEmailSettingsSchema.nullish(),
emailSettings: ZDocumentEmailSettingsInputSchema.nullish(),
envelopeExpirationPeriod: ZEnvelopeExpirationPeriod.nullish(),
reminderSettings: ZEnvelopeReminderSettings.nullish(),
});
@@ -1,26 +0,0 @@
export type OrganisationLimits = {
documentQuota: number | null;
emailQuota: number | null;
apiQuota: number | null;
teamCount: number;
memberCount: number;
recipientCount: number;
envelopeItemCount: number;
};
const ORGANISATION_LIMIT_KEYS = [
'documentQuota',
'emailQuota',
'apiQuota',
'teamCount',
'memberCount',
'recipientCount',
'envelopeItemCount',
] as const satisfies (keyof OrganisationLimits)[];
export const hasOrganisationLimitsChanged = (
previous: OrganisationLimits,
next: Partial<OrganisationLimits>,
): boolean => {
return ORGANISATION_LIMIT_KEYS.some((key) => next[key] !== undefined && next[key] !== previous[key]);
};
@@ -32,7 +32,7 @@ type FindOrganisationStatsOptions = {
claimId?: string;
page?: number;
perPage?: number;
orderByColumn?: 'documentCount' | 'emailCount' | 'apiCount' | 'emailReports' | 'teamCount' | 'totalCount';
orderByColumn?: 'documentCount' | 'emailCount' | 'apiCount' | 'emailReports' | 'totalCount';
orderByDirection?: 'asc' | 'desc';
};
@@ -95,12 +95,6 @@ export const findOrganisationStats = async ({
'OrganisationClaim.documentQuota as documentQuota',
'OrganisationClaim.emailQuota as emailQuota',
'OrganisationClaim.apiQuota as apiQuota',
// Teams have no monthly history, so this is the current count for every period.
eb
.selectFrom('Team')
.whereRef('Team.organisationId', '=', 'Organisation.id')
.select(sql<number>`count("Team"."id")`.as('count'))
.as('teamCount'),
totalCountExpression.as('totalCount'),
eb.fn.countAll().over().as('totalRows'),
])
@@ -110,7 +104,6 @@ export const findOrganisationStats = async ({
.with('emailCount', () => qb.orderBy('OrganisationMonthlyStat.emailCount', orderByDirection))
.with('apiCount', () => qb.orderBy('OrganisationMonthlyStat.apiCount', orderByDirection))
.with('emailReports', () => qb.orderBy('OrganisationMonthlyStat.emailReports', orderByDirection))
.with('teamCount', () => qb.orderBy('teamCount', orderByDirection))
.with('totalCount', () => qb.orderBy(totalCountExpression, orderByDirection))
.with(undefined, () =>
// Default ordering mirrors the desired SQL: email, api, document descending.
@@ -139,7 +132,6 @@ export const findOrganisationStats = async ({
emailCount: Number(row.emailCount),
apiCount: Number(row.apiCount),
emailReports: Number(row.emailReports),
teamCount: Number(row.teamCount),
documentQuota: row.documentQuota === null ? null : Number(row.documentQuota),
emailQuota: row.emailQuota === null ? null : Number(row.emailQuota),
apiQuota: row.apiQuota === null ? null : Number(row.apiQuota),
@@ -9,7 +9,7 @@ export const ZFindOrganisationStatsRequestSchema = ZFindSearchParamsSchema.exten
.optional(),
claimId: z.string().describe('Filter stats by the original subscription claim ID.').optional(),
orderByColumn: z
.enum(['documentCount', 'emailCount', 'apiCount', 'emailReports', 'teamCount', 'totalCount'])
.enum(['documentCount', 'emailCount', 'apiCount', 'emailReports', 'totalCount'])
.describe('The column to sort by.')
.optional(),
orderByDirection: z.enum(['asc', 'desc']).describe('Sort direction.').default('desc'),
@@ -27,7 +27,6 @@ export const ZFindOrganisationStatsResponseSchema = ZFindResultResponse.extend({
emailCount: z.number(),
apiCount: z.number(),
emailReports: z.number(),
teamCount: z.number(),
documentQuota: z.number().nullable(),
emailQuota: z.number().nullable(),
apiQuota: z.number().nullable(),
@@ -1,6 +1,4 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { jobs } from '@documenso/lib/jobs/client';
import { hasOrganisationLimitsChanged } from '@documenso/lib/universal/organisation-limit-changes';
import { prisma } from '@documenso/prisma';
import { adminProcedure } from '../trpc';
@@ -13,12 +11,11 @@ export const updateAdminOrganisationRoute = adminProcedure
.input(ZUpdateAdminOrganisationRequestSchema)
.output(ZUpdateAdminOrganisationResponseSchema)
.mutation(async ({ input, ctx }) => {
const { organisationId, data, notifyOrganisation } = input;
const { organisationId, data } = input;
ctx.logger.info({
input: {
organisationId,
notifyOrganisation,
},
});
@@ -37,8 +34,6 @@ export const updateAdminOrganisationRoute = adminProcedure
const { name, url, customerId, claims, originalSubscriptionClaimId } = data;
const isLimitsChanged = claims ? hasOrganisationLimitsChanged(organisation.organisationClaim, claims) : false;
await prisma.organisation.update({
where: {
id: organisationId,
@@ -59,26 +54,4 @@ export const updateAdminOrganisationRoute = adminProcedure
originalSubscriptionClaimId,
},
});
if (!notifyOrganisation) {
return { isNotificationSent: false };
}
if (!isLimitsChanged) {
ctx.logger.info({
msg: 'Skipping organisation limits updated email, no limits changed',
organisationId,
});
return { isNotificationSent: false };
}
await jobs.triggerJob({
name: 'send.organisation-limits-updated.email',
payload: {
organisationId,
},
});
return { isNotificationSent: true };
});
@@ -26,11 +26,8 @@ export const ZUpdateAdminOrganisationRequestSchema = z.object({
customerId: z.string().optional(),
originalSubscriptionClaimId: z.string().optional(),
}),
notifyOrganisation: z.boolean().optional(),
});
export const ZUpdateAdminOrganisationResponseSchema = z.object({
isNotificationSent: z.boolean(),
});
export const ZUpdateAdminOrganisationResponseSchema = z.void();
export type TUpdateAdminOrganisationRequest = z.infer<typeof ZUpdateAdminOrganisationRequestSchema>;
@@ -1,5 +1,5 @@
import { ZDocumentLiteSchema } from '@documenso/lib/types/document';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
@@ -39,7 +39,7 @@ export const ZDistributeDocumentRequestSchema = z.object({
language: ZDocumentMetaLanguageSchema.optional(),
emailId: z.string().nullish(),
emailReplyTo: zEmail().nullish(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
emailSettings: ZDocumentEmailSettingsInputSchema.optional(),
})
.optional(),
});
@@ -1,4 +1,4 @@
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
@@ -66,7 +66,7 @@ export const ZCreateEmbeddingDocumentRequestSchema = z.object({
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
emailSettings: ZDocumentEmailSettingsInputSchema.optional(),
})
.optional(),
});
@@ -1,4 +1,4 @@
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
@@ -62,7 +62,7 @@ export const ZCreateEmbeddingTemplateRequestSchema = z.object({
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
emailSettings: ZDocumentEmailSettingsInputSchema.optional(),
})
.optional(),
});
@@ -1,4 +1,4 @@
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
@@ -67,7 +67,7 @@ export const ZUpdateEmbeddingDocumentRequestSchema = z.object({
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
emailSettings: ZDocumentEmailSettingsInputSchema.optional(),
})
.optional(),
});
@@ -1,4 +1,4 @@
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
@@ -66,7 +66,7 @@ export const ZUpdateEmbeddingTemplateRequestSchema = z.object({
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
emailSettings: ZDocumentEmailSettingsInputSchema.optional(),
})
.optional(),
});
@@ -1,5 +1,5 @@
import { ZEnvelopeExpirationPeriod } from '@documenso/lib/constants/envelope-expiration';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email';
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
import {
ZDocumentMetaDateFormatSchema,
@@ -89,7 +89,7 @@ export const ZUseEnvelopePayloadSchema = z.object({
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
emailSettings: ZDocumentEmailSettingsInputSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
@@ -4,7 +4,7 @@ import { ZEnvelopeReminderSettings } from '@documenso/lib/constants/envelope-rem
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { ZCssVarsSchema } from '@documenso/lib/types/css-vars';
import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email';
import { ZDocumentMetaDateFormatSchema, ZDocumentMetaTimezoneSchema } from '@documenso/lib/types/document-meta';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { ZSanitizeBrandingCssWarningSchema } from '@documenso/lib/utils/sanitize-branding-css';
@@ -41,7 +41,7 @@ export const ZUpdateOrganisationSettingsRequestSchema = z.object({
emailId: z.string().nullish(),
emailReplyTo: zEmail().nullish(),
// emailReplyToName: z.string().optional(),
emailDocumentSettings: ZDocumentEmailSettingsSchema.optional(),
emailDocumentSettings: ZDocumentEmailSettingsInputSchema.optional(),
// AI features settings.
aiFeaturesEnabled: z.boolean().optional(),
@@ -4,7 +4,7 @@ import { ZEnvelopeReminderSettings } from '@documenso/lib/constants/envelope-rem
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { ZCssVarsSchema } from '@documenso/lib/types/css-vars';
import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email';
import { ZDocumentMetaDateFormatSchema, ZDocumentMetaTimezoneSchema } from '@documenso/lib/types/document-meta';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { ZSanitizeBrandingCssWarningSchema } from '@documenso/lib/utils/sanitize-branding-css';
@@ -44,7 +44,7 @@ export const ZUpdateTeamSettingsRequestSchema = z.object({
emailId: z.string().nullish(),
emailReplyTo: zEmail().nullish(),
// emailReplyToName: z.string().nullish(),
emailDocumentSettings: ZDocumentEmailSettingsSchema.nullish(),
emailDocumentSettings: ZDocumentEmailSettingsInputSchema.nullish(),
// Default recipients settings.
defaultRecipients: ZDefaultRecipientsSchema.nullish(),
@@ -1,7 +1,7 @@
import { ZEnvelopeExpirationPeriod } from '@documenso/lib/constants/envelope-expiration';
import { ZDocumentSchema } from '@documenso/lib/types/document';
import { ZDocumentAccessAuthTypesSchema, ZDocumentActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email';
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
import {
ZDocumentMetaDateFormatSchema,
@@ -60,7 +60,7 @@ export const ZTemplateMetaUpsertSchema = z.object({
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
emailId: z.string().nullish(),
emailReplyTo: zEmail().nullish(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
emailSettings: ZDocumentEmailSettingsInputSchema.optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
@@ -142,7 +142,7 @@ export const ZCreateDocumentFromTemplateRequestSchema = z.object({
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
emailSettings: ZDocumentEmailSettingsInputSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
-7
View File
@@ -133,13 +133,6 @@ declare namespace NodeJS {
NEXT_PUBLIC_TURNSTILE_SITE_KEY?: string;
NEXT_PRIVATE_TURNSTILE_SECRET_KEY?: string;
/**
* Legal & Data Protection environment variables
*/
NEXT_PUBLIC_TERMS_OF_SERVICE_URL?: string;
NEXT_PUBLIC_PRIVACY_POLICY_URL?: string;
NEXT_PUBLIC_IMPRINT_URL?: string;
/**
* Google Vertex AI environment variables
*/
@@ -21,6 +21,33 @@ export const DocumentEmailCheckboxes = ({ value, onChange, className, hiddenEven
return (
<div className={cn('space-y-3', className)}>
<div className="flex flex-row items-center">
<Checkbox
id="attachDocument"
className="h-5 w-5"
checked={value.attachDocument ?? true}
onCheckedChange={(checked) => onChange({ ...value, attachDocument: Boolean(checked) })}
/>
<label className="ml-2 flex flex-row items-center text-muted-foreground text-sm" htmlFor="attachDocument">
<Trans>Attach completed document PDFs to emails</Trans>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
<p>
<Trans>
Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable
this to send emails with document links only.
</Trans>
</p>
</TooltipContent>
</Tooltip>
</label>
</div>
{!isHidden(DocumentEmailEvents.RecipientSigned) && (
<div className="flex flex-row items-center">
<Checkbox