mirror of
https://github.com/documenso/documenso.git
synced 2026-10-03 02:04:40 +10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5147acd09 | ||
|
|
b8e6ba1c12 | ||
|
|
917c698160 | ||
|
|
b4f5238206 |
@@ -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
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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'}
|
||||
|
||||
-65
@@ -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
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Vendored
-7
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user