Compare commits

..

14 Commits

Author SHA1 Message Date
Catalin Pit bb62fa7a16 Merge branch 'archive/nextjs' into feat/customize-doc-audit-log-certificate 2025-06-11 13:57:28 +03:00
Jenil Savani ad01e5af94 fix: adjust desktop nav search button width and spacing (#1699) 2025-03-13 10:35:24 +11:00
Ephraim Duncan 8890c5bee6 fix: signing field disabled when pointer is out of canvas (#1652) 2025-03-12 16:43:52 +11:00
Jenil Savani 6f39e89d30 fix: improve layout and truncate document information in logs page (#1656) 2025-03-12 16:29:48 +11:00
Tom db90e1a34a chore: update French translations (#1679) 2025-03-12 16:16:44 +11:00
Catalin Pit 9182d014b4 Merge branch 'main' into feat/customize-doc-audit-log-certificate 2025-02-24 09:36:31 +02:00
Catalin Pit 5f602d897b chore: fix seal document handler 2025-02-20 10:50:59 +02:00
Catalin Pit 0084a94bb1 chore: improve logic 2025-02-19 15:04:44 +02:00
Catalin Pit a4f1a138d0 chore: redo changes 2025-02-17 15:11:16 +02:00
Catalin Pit 6a9ae132c7 Merge branch 'main' into feat/customize-doc-audit-log-certificate 2025-02-17 11:19:18 +02:00
Catalin Pit afb156f073 chore: audit trail log cert 2025-02-17 11:18:41 +02:00
Catalin Pit f6a24224fe feat: download options for document 2025-02-14 16:25:15 +02:00
Catalin Pit 080bb405f0 feat: include audit logs 2025-02-14 14:03:39 +02:00
Catalin Pit 8b1b0de935 feat: customize doc audit logs and certificate 2025-02-14 11:30:48 +02:00
28 changed files with 745 additions and 110 deletions
@@ -14,6 +14,12 @@ import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import { trpc as trpcClient } from '@documenso/trpc/client';
import { Button } from '@documenso/ui/primitives/button';
import {
SplitButton,
SplitButtonAction,
SplitButtonDropdown,
SplitButtonDropdownItem,
} from '@documenso/ui/primitives/split-button';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type DocumentPageViewButtonProps = {
@@ -25,7 +31,9 @@ export type DocumentPageViewButtonProps = {
team?: Pick<Team, 'id' | 'url'>;
};
export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps) => {
export const DocumentPageViewButton = ({
document: activeDocument,
}: DocumentPageViewButtonProps) => {
const { data: session } = useSession();
const { toast } = useToast();
const { _ } = useLingui();
@@ -34,25 +42,27 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
return null;
}
const recipient = document.recipients.find((recipient) => recipient.email === session.user.email);
const recipient = activeDocument.recipients.find(
(recipient) => recipient.email === session.user.email,
);
const isRecipient = !!recipient;
const isPending = document.status === DocumentStatus.PENDING;
const isComplete = document.status === DocumentStatus.COMPLETED;
const isPending = activeDocument.status === DocumentStatus.PENDING;
const isComplete = activeDocument.status === DocumentStatus.COMPLETED;
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
const role = recipient?.role;
const documentsPath = formatDocumentsPath(document.team?.url);
const documentsPath = formatDocumentsPath(activeDocument.team?.url);
const onDownloadClick = async () => {
try {
const documentWithData = await trpcClient.document.getDocumentById.query(
{
documentId: document.id,
documentId: activeDocument.id,
},
{
context: {
teamId: document.team?.id?.toString(),
teamId: activeDocument.team?.id?.toString(),
},
},
);
@@ -63,7 +73,10 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
throw new Error('No document available');
}
await downloadPDF({ documentData, fileName: documentWithData.title });
await downloadPDF({
documentData,
fileName: documentWithData.title,
});
} catch (err) {
toast({
title: _(msg`Something went wrong`),
@@ -73,6 +86,100 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
}
};
const onDownloadAuditLogClick = async () => {
try {
const { url } = await trpcClient.document.downloadAuditLogs.mutate({
documentId: activeDocument.id,
});
const iframe = Object.assign(document.createElement('iframe'), {
src: url,
});
Object.assign(iframe.style, {
position: 'fixed',
top: '0',
left: '0',
width: '0',
height: '0',
});
const onLoaded = () => {
if (iframe.contentDocument?.readyState === 'complete') {
iframe.contentWindow?.print();
iframe.contentWindow?.addEventListener('afterprint', () => {
document.body.removeChild(iframe);
});
}
};
// When the iframe has loaded, print the iframe and remove it from the dom
iframe.addEventListener('load', onLoaded);
document.body.appendChild(iframe);
onLoaded();
} catch (error) {
console.error(error);
toast({
title: _(msg`Something went wrong`),
description: _(
msg`Sorry, we were unable to download the audit logs. Please try again later.`,
),
variant: 'destructive',
});
}
};
const onDownloadSigningCertificateClick = async () => {
try {
const { url } = await trpcClient.document.downloadCertificate.mutate({
documentId: activeDocument.id,
});
const iframe = Object.assign(document.createElement('iframe'), {
src: url,
});
Object.assign(iframe.style, {
position: 'fixed',
top: '0',
left: '0',
width: '0',
height: '0',
});
const onLoaded = () => {
if (iframe.contentDocument?.readyState === 'complete') {
iframe.contentWindow?.print();
iframe.contentWindow?.addEventListener('afterprint', () => {
document.body.removeChild(iframe);
});
}
};
// When the iframe has loaded, print the iframe and remove it from the dom
iframe.addEventListener('load', onLoaded);
document.body.appendChild(iframe);
onLoaded();
} catch (error) {
console.error(error);
toast({
title: _(msg`Something went wrong`),
description: _(
msg`Sorry, we were unable to download the certificate. Please try again later.`,
),
variant: 'destructive',
});
}
};
return match({
isRecipient,
isPending,
@@ -106,16 +213,27 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
))
.with({ isComplete: false }, () => (
<Button className="w-full" asChild>
<Link href={`${documentsPath}/${document.id}/edit`}>
<Link href={`${documentsPath}/${activeDocument.id}/edit`}>
<Trans>Edit</Trans>
</Link>
</Button>
))
.with({ isComplete: true }, () => (
<Button className="w-full" onClick={onDownloadClick}>
<Download className="-ml-1 mr-2 inline h-4 w-4" />
<Trans>Download</Trans>
</Button>
<SplitButton className="flex w-full">
<SplitButtonAction className="w-full" onClick={() => void onDownloadClick()}>
<Download className="-ml-1 mr-2 inline h-4 w-4" />
<Trans>Download</Trans>
</SplitButtonAction>
<SplitButtonDropdown>
<SplitButtonDropdownItem onClick={() => void onDownloadAuditLogClick()}>
<Trans>Only Audit Log</Trans>
</SplitButtonDropdownItem>
<SplitButtonDropdownItem onClick={() => void onDownloadSigningCertificateClick()}>
<Trans>Only Signing Certificate</Trans>
</SplitButtonDropdownItem>
</SplitButtonDropdown>
</SplitButton>
))
.otherwise(() => null);
};
@@ -187,6 +187,8 @@ export const EditDocumentForm = ({
title: data.title,
externalId: data.externalId || null,
visibility: data.visibility,
includeSigningCertificate: data.includeSigningCertificate,
includeAuditTrailLog: data.includeAuditTrailLog,
globalAccessAuth: data.globalAccessAuth ?? null,
globalActionAuth: data.globalActionAuth ?? null,
},
@@ -119,7 +119,7 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
<Trans>Document</Trans>
</Link>
<div className="flex flex-col justify-between truncate sm:flex-row">
<div className="flex flex-col">
<div>
<h1
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
@@ -127,7 +127,8 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
>
{document.title}
</h1>
</div>
<div className="mt-1 flex flex-col justify-between sm:flex-row">
<div className="mt-2.5 flex items-center gap-x-6">
<DocumentStatusComponent
inheritColor
@@ -135,17 +136,16 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
className="text-muted-foreground"
/>
</div>
</div>
<div className="mt-4 flex w-full flex-row sm:mt-0 sm:w-auto sm:self-end">
<DownloadCertificateButton
className="mr-2"
documentId={document.id}
documentStatus={document.status}
teamId={team?.id}
/>
<div className="mt-4 flex w-full flex-row sm:mt-0 sm:w-auto sm:self-end">
<DownloadCertificateButton
className="mr-2"
documentId={document.id}
documentStatus={document.status}
teamId={team?.id}
/>
<DownloadAuditLogButton teamId={team?.id} documentId={document.id} />
<DownloadAuditLogButton teamId={team?.id} documentId={document.id} />
</div>
</div>
</div>
@@ -154,7 +154,7 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
{documentInformation.map((info, i) => (
<div className="text-foreground text-sm" key={i}>
<h3 className="font-semibold">{_(info.description)}</h3>
<p className="text-muted-foreground">{info.value}</p>
<p className="text-muted-foreground truncate">{info.value}</p>
</div>
))}
@@ -41,6 +41,7 @@ const ZTeamDocumentPreferencesFormSchema = z.object({
includeSenderDetails: z.boolean(),
typedSignatureEnabled: z.boolean(),
includeSigningCertificate: z.boolean(),
includeAuditTrailLog: z.boolean(),
});
type TTeamDocumentPreferencesFormSchema = z.infer<typeof ZTeamDocumentPreferencesFormSchema>;
@@ -72,6 +73,7 @@ export const TeamDocumentPreferencesForm = ({
includeSenderDetails: settings?.includeSenderDetails ?? false,
typedSignatureEnabled: settings?.typedSignatureEnabled ?? true,
includeSigningCertificate: settings?.includeSigningCertificate ?? true,
includeAuditTrailLog: settings?.includeAuditTrailLog ?? false,
},
resolver: zodResolver(ZTeamDocumentPreferencesFormSchema),
});
@@ -86,6 +88,7 @@ export const TeamDocumentPreferencesForm = ({
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
includeAuditTrailLog,
} = data;
await updateTeamDocumentPreferences({
@@ -96,6 +99,7 @@ export const TeamDocumentPreferencesForm = ({
includeSenderDetails,
typedSignatureEnabled,
includeSigningCertificate,
includeAuditTrailLog,
},
});
@@ -300,6 +304,37 @@ export const TeamDocumentPreferencesForm = ({
)}
/>
<FormField
control={form.control}
name="includeAuditTrailLog"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>
<Trans>Include the Audit Trail Log in the Document</Trans>
</FormLabel>
<div>
<FormControl className="block">
<Switch
ref={field.ref}
name={field.name}
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</div>
<FormDescription>
<Trans>
Controls whether the audit trail log will be included in the document when it is
downloaded. The audit trail log can still be downloaded from the logs page
separately.
</Trans>
</FormDescription>
</FormItem>
)}
/>
<div className="flex flex-row justify-end space-x-4">
<Button type="submit" loading={form.formState.isSubmitting}>
<Trans>Save</Trans>
@@ -73,7 +73,7 @@ export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: Deskto
<Button
variant="outline"
className="text-muted-foreground flex w-96 items-center justify-between rounded-lg"
className="text-muted-foreground flex w-full max-w-96 items-center justify-between rounded-lg"
onClick={() => setIsCommandMenuOpen(true)}
>
<div className="flex items-center">
@@ -82,7 +82,7 @@ export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: Deskto
</div>
<div>
<div className="text-muted-foreground bg-muted flex items-center rounded-md px-1.5 py-0.5 text-xs tracking-wider">
<div className="text-muted-foreground bg-muted flex items-center rounded-md px-1.5 py-0.5 text-xs tracking-wider">
{modifierKey}+K
</div>
</div>
@@ -363,6 +363,40 @@ export const DocumentHistorySheet = ({
]}
/>
))
.with(
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_CERTIFICATE_UPDATED },
({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Old',
value: data.from,
},
{
key: 'New',
value: data.to,
},
]}
/>
),
)
.with(
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_AUDIT_TRAIL_UPDATED },
({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Old',
value: data.from,
},
{
key: 'New',
value: data.to,
},
]}
/>
),
)
.exhaustive()}
{isUserDetailsVisible && (
@@ -17,6 +17,7 @@ const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
documentLanguage: z.string(),
includeSenderDetails: z.boolean(),
includeSigningCertificate: z.boolean(),
includeAuditTrailLog: z.boolean(),
brandingEnabled: z.boolean(),
brandingLogo: z.string(),
brandingUrl: z.string(),
@@ -13,6 +13,7 @@ import { signPdf } from '@documenso/signing';
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
import PostHogServerClient from '../../../server-only/feature-flags/get-post-hog-server-client';
import { getAuditLogsPdf } from '../../../server-only/htmltopdf/get-audit-logs-pdf';
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations';
import { flattenForm } from '../../../server-only/pdf/flatten-form';
@@ -57,6 +58,7 @@ export const run = async ({
teamGlobalSettings: {
select: {
includeSigningCertificate: true,
includeAuditTrailLog: true,
},
},
},
@@ -121,13 +123,36 @@ export const run = async ({
const pdfData = await getFile(documentData);
const certificateData =
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
? await getCertificatePdf({
documentId,
language: document.documentMeta?.language,
}).catch(() => null)
: null;
let includeSigningCertificate;
if (document.teamId) {
includeSigningCertificate =
document.team?.teamGlobalSettings?.includeSigningCertificate ?? true;
} else {
includeSigningCertificate = document.includeSigningCertificate ?? true;
}
const certificateData = includeSigningCertificate
? await getCertificatePdf({
documentId,
language: document.documentMeta?.language,
}).catch(() => null)
: null;
let includeAuditTrailLog;
if (document.teamId) {
includeAuditTrailLog = document.team?.teamGlobalSettings?.includeAuditTrailLog ?? true;
} else {
includeAuditTrailLog = document.includeAuditTrailLog ?? true;
}
const auditLogData = includeAuditTrailLog
? await getAuditLogsPdf({
documentId,
language: document.documentMeta?.language,
}).catch(() => null)
: null;
const newDataId = await io.runTask('decorate-and-sign-pdf', async () => {
const pdfDoc = await PDFDocument.load(pdfData);
@@ -150,6 +175,16 @@ export const run = async ({
});
}
if (auditLogData) {
const auditLog = await PDFDocument.load(auditLogData);
const auditLogPages = await pdfDoc.copyPages(auditLog, auditLog.getPageIndices());
auditLogPages.forEach((page) => {
pdfDoc.addPage(page);
});
}
for (const field of fields) {
if (field.inserted) {
await insertFieldInPDF(pdfDoc, field);
@@ -124,6 +124,8 @@ export const createDocument = async ({
team?.teamGlobalSettings?.documentVisibility,
userTeamRole ?? TeamMemberRole.MEMBER,
),
includeSigningCertificate: team?.teamGlobalSettings?.includeSigningCertificate ?? true,
includeAuditTrailLog: team?.teamGlobalSettings?.includeAuditTrailLog ?? true,
formValues,
source: DocumentSource.DOCUMENT,
documentMeta: {
@@ -22,6 +22,7 @@ import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getFile } from '../../universal/upload/get-file';
import { putPdfFile } from '../../universal/upload/put-file';
import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers';
import { getAuditLogsPdf } from '../htmltopdf/get-audit-logs-pdf';
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
import { flattenAnnotations } from '../pdf/flatten-annotations';
import { flattenForm } from '../pdf/flatten-form';
@@ -61,6 +62,7 @@ export const sealDocument = async ({
teamGlobalSettings: {
select: {
includeSigningCertificate: true,
includeAuditTrailLog: true,
},
},
},
@@ -109,13 +111,36 @@ export const sealDocument = async ({
// !: Need to write the fields onto the document as a hard copy
const pdfData = await getFile(documentData);
const certificateData =
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
? await getCertificatePdf({
documentId,
language: document.documentMeta?.language,
}).catch(() => null)
: null;
let includeSigningCertificate;
if (document.teamId) {
includeSigningCertificate =
document.team?.teamGlobalSettings?.includeSigningCertificate ?? true;
} else {
includeSigningCertificate = document.includeSigningCertificate ?? true;
}
const certificateData = includeSigningCertificate
? await getCertificatePdf({
documentId,
language: document.documentMeta?.language,
}).catch(() => null)
: null;
let includeAuditTrailLog;
if (document.teamId) {
includeAuditTrailLog = document.team?.teamGlobalSettings?.includeAuditTrailLog ?? true;
} else {
includeAuditTrailLog = document.includeAuditTrailLog ?? true;
}
const auditLogData = includeAuditTrailLog
? await getAuditLogsPdf({
documentId,
language: document.documentMeta?.language,
}).catch(() => null)
: null;
const doc = await PDFDocument.load(pdfData);
@@ -134,6 +159,16 @@ export const sealDocument = async ({
});
}
if (auditLogData) {
const auditLog = await PDFDocument.load(auditLogData);
const auditLogPages = await doc.copyPages(auditLog, auditLog.getPageIndices());
auditLogPages.forEach((page) => {
doc.addPage(page);
});
}
for (const field of fields) {
await insertFieldInPDF(doc, field);
}
@@ -21,6 +21,8 @@ export type UpdateDocumentOptions = {
title?: string;
externalId?: string | null;
visibility?: DocumentVisibility | null;
includeSigningCertificate?: boolean;
includeAuditTrailLog?: boolean;
globalAccessAuth?: TDocumentAccessAuthTypes | null;
globalActionAuth?: TDocumentActionAuthTypes | null;
};
@@ -156,6 +158,12 @@ export const updateDocument = async ({
documentGlobalActionAuth === undefined || documentGlobalActionAuth === newGlobalActionAuth;
const isDocumentVisibilitySame =
data.visibility === undefined || data.visibility === document.visibility;
const isIncludeSigningCertificateSame =
data.includeSigningCertificate === undefined ||
data.includeSigningCertificate === document.includeSigningCertificate;
const isIncludeAuditTrailLogSame =
data.includeAuditTrailLog === undefined ||
data.includeAuditTrailLog === document.includeAuditTrailLog;
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
@@ -235,6 +243,34 @@ export const updateDocument = async ({
);
}
if (!isIncludeSigningCertificateSame) {
auditLogs.push(
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_CERTIFICATE_UPDATED,
documentId,
metadata: requestMetadata,
data: {
from: String(document.includeSigningCertificate),
to: String(data.includeSigningCertificate || false),
},
}),
);
}
if (!isIncludeAuditTrailLogSame) {
auditLogs.push(
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_AUDIT_TRAIL_UPDATED,
documentId,
metadata: requestMetadata,
data: {
from: String(document.includeAuditTrailLog),
to: String(data.includeAuditTrailLog || false),
},
}),
);
}
// Early return if nothing is required.
if (auditLogs.length === 0) {
return document;
@@ -254,6 +290,8 @@ export const updateDocument = async ({
title: data.title,
externalId: data.externalId,
visibility: data.visibility as DocumentVisibility,
includeSigningCertificate: data.includeSigningCertificate,
includeAuditTrailLog: data.includeAuditTrailLog,
authOptions,
},
});
@@ -0,0 +1,74 @@
import { DateTime } from 'luxon';
import type { Browser } from 'playwright';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { type SupportedLanguageCodes, isValidLanguageCode } from '../../constants/i18n';
import { encryptSecondaryData } from '../crypto/encrypt';
export type GetAuditLogsPdfParams = {
documentId: number;
// eslint-disable-next-line @typescript-eslint/ban-types
language?: SupportedLanguageCodes | (string & {});
};
export const getAuditLogsPdf = async ({ documentId, language }: GetAuditLogsPdfParams) => {
const { chromium } = await import('playwright');
const encryptedId = encryptSecondaryData({
data: documentId.toString(),
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
});
let browser: Browser;
if (process.env.NEXT_PRIVATE_BROWSERLESS_URL) {
// !: Use CDP rather than the default `connect` method to avoid coupling to the playwright version.
// !: Previously we would have to keep the playwright version in sync with the browserless version to avoid errors.
browser = await chromium.connectOverCDP(process.env.NEXT_PRIVATE_BROWSERLESS_URL);
} else {
browser = await chromium.launch();
}
if (!browser) {
throw new Error(
'Failed to establish a browser, please ensure you have either a Browserless.io url or chromium browser installed',
);
}
const browserContext = await browser.newContext();
const page = await browserContext.newPage();
const lang = isValidLanguageCode(language) ? language : 'en';
await page.context().addCookies([
{
name: 'language',
value: lang,
url: NEXT_PUBLIC_WEBAPP_URL(),
},
]);
try {
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/audit-log?d=${encryptedId}`, {
waitUntil: 'networkidle',
timeout: 10_000,
});
const result = await page.pdf({
format: 'A4',
});
await browserContext.close();
void browser.close();
return result;
} catch (error) {
await browserContext.close();
void browser.close();
throw error;
}
};
@@ -17,6 +17,7 @@ export type UpdateTeamDocumentSettingsOptions = {
includeSenderDetails: boolean;
typedSignatureEnabled: boolean;
includeSigningCertificate: boolean;
includeAuditTrailLog: boolean;
};
};
@@ -36,6 +37,7 @@ export const updateTeamDocumentSettings = async ({
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
includeAuditTrailLog,
typedSignatureEnabled,
} = settings;
@@ -61,6 +63,7 @@ export const updateTeamDocumentSettings = async ({
includeSenderDetails,
typedSignatureEnabled,
includeSigningCertificate,
includeAuditTrailLog,
},
update: {
documentVisibility,
@@ -68,6 +71,7 @@ export const updateTeamDocumentSettings = async ({
includeSenderDetails,
typedSignatureEnabled,
includeSigningCertificate,
includeAuditTrailLog,
},
});
};
+41 -41
View File
@@ -360,7 +360,7 @@ msgstr "<0>{teamName}</0> a demandé à utiliser votre adresse e-mail pour leur
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:463
msgid "<0>Click to upload</0> or drag and drop"
msgstr "<0>Cliquez pour télécharger</0> ou faites glisser et déposez"
msgstr "<0>Cliquez pour importer</0> ou faites glisser et déposez"
#: packages/ui/primitives/template-flow/add-template-settings.tsx:287
msgid "<0>Email</0> - The recipient will be emailed the document to sign, approve, etc."
@@ -1009,7 +1009,7 @@ msgstr "Une erreur est survenue lors de la mise à jour de votre profil."
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:108
msgid "An error occurred while uploading your document."
msgstr "Une erreur est survenue lors du téléchargement de votre document."
msgstr "Une erreur est survenue lors de l'importation de votre document."
#: apps/web/src/app/(dashboard)/admin/documents/[id]/super-delete-document-dialog.tsx:58
#: apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx:81
@@ -1430,7 +1430,7 @@ msgstr "Cliquez ici pour réessayer"
#: apps/web/src/components/(teams)/dialogs/invite-team-member-dialog.tsx:392
msgid "Click here to upload"
msgstr "Cliquez ici pour télécharger"
msgstr "Cliquez ici pour importer"
#: apps/web/src/components/(dashboard)/avatar/avatar-with-recipient.tsx:52
#: apps/web/src/components/(dashboard)/avatar/avatar-with-recipient.tsx:65
@@ -1597,11 +1597,11 @@ msgstr "Continuer vers la connexion"
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:185
msgid "Controls the default language of an uploaded document. This will be used as the language in email communications with the recipients."
msgstr "Contrôle la langue par défaut d'un document téléchargé. Cela sera utilisé comme langue dans les communications par e-mail avec les destinataires."
msgstr "Contrôle la langue par défaut d'un document importé. Cela sera utilisé comme langue dans les communications par e-mail avec les destinataires."
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:153
msgid "Controls the default visibility of an uploaded document."
msgstr "Contrôle la visibilité par défaut d'un document téléchargé."
msgstr "Contrôle la visibilité par défaut d'un document importé."
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:232
msgid "Controls the formatting of the message that will be sent when inviting a recipient to sign a document. If a custom message has been provided while configuring the document, it will be used instead."
@@ -2255,11 +2255,11 @@ msgstr "Document mis à jour"
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:55
msgid "Document upload disabled due to unpaid invoices"
msgstr "Téléchargement du document désactivé en raison de factures impayées"
msgstr "Importation de documents désactivé en raison de factures impayées"
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:85
msgid "Document uploaded"
msgstr "Document téléchargé"
msgstr "Document importé"
#: apps/web/src/app/(signing)/sign/[token]/complete/page.tsx:131
msgid "Document Viewed"
@@ -2721,7 +2721,7 @@ msgstr "Pour toute question concernant cette divulgation, les signatures électr
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:147
msgid "For each recipient, provide their email (required) and name (optional) in separate columns. Download the template CSV below for the correct format."
msgstr "Pour chaque destinataire, fournissez leur email (obligatoire) et leur nom (facultatif) dans des colonnes séparées. Téléchargez le modèle CSV ci-dessous pour le format correct."
msgstr "Pour chaque destinataire, fournissez son e-mail (obligatoire) et son nom (facultatif) dans des colonnes séparées. Téléchargez le modèle CSV ci-dessous pour obtenir le format requis."
#: packages/lib/server-only/auth/send-forgot-password.ts:61
msgid "Forgot Password?"
@@ -2770,7 +2770,7 @@ msgstr "Authentification d'action de destinataire globale"
#: apps/web/src/components/partials/not-found.tsx:67
#: packages/ui/primitives/document-flow/document-flow-root.tsx:142
msgid "Go Back"
msgstr "Retourner"
msgstr "Retour"
#: apps/web/src/app/(unauthenticated)/verify-email/[token]/client.tsx:48
#: apps/web/src/app/(unauthenticated)/verify-email/[token]/page.tsx:73
@@ -2834,7 +2834,7 @@ msgstr "Salut, je suis Timur"
#: packages/email/templates/bulk-send-complete.tsx:36
msgid "Hi {userName},"
msgstr "Bonjour, {userName},"
msgstr "Bonjour {userName},"
#: packages/email/templates/reset-password.tsx:56
msgid "Hi, {userName} <0>({userEmail})</0>"
@@ -2856,7 +2856,7 @@ msgstr "Je suis un signataire de ce document"
#: packages/lib/constants/recipient-roles.ts:47
msgid "I am a viewer of this document"
msgstr "Je suis un visualiseur de ce document"
msgstr "Je suis un lecteur de ce document"
#: packages/lib/constants/recipient-roles.ts:45
msgid "I am an approver of this document"
@@ -3234,11 +3234,11 @@ msgstr "MAU (document terminé)"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx:209
msgid "Max"
msgstr ""
msgstr "Maximum"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:227
msgid "Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use template defaults."
msgstr "Taille maximale du fichier : 4 Mo. Maximum de 100 lignes par téléversement. Les valeurs vides utiliseront les valeurs par défaut du modèle."
msgstr "Taille maximale du fichier : 4 Mo. Maximum de 100 lignes par importation. Les valeurs vides utiliseront les valeurs par défaut du modèle."
#: packages/lib/constants/teams.ts:12
msgid "Member"
@@ -5032,7 +5032,7 @@ msgstr "Modèle supprimé"
#: apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx:66
msgid "Template document uploaded"
msgstr "Document modèle téléchargé"
msgstr "Document modèle importé"
#: apps/web/src/app/(dashboard)/templates/duplicate-template-dialog.tsx:41
msgid "Template duplicated"
@@ -5097,11 +5097,11 @@ msgstr "Couleur du texte"
#: apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx:24
msgid "Thank you for using Documenso to perform your electronic document signing. The purpose of this disclosure is to inform you about the process, legality, and your rights regarding the use of electronic signatures on our platform. By opting to use an electronic signature, you are agreeing to the terms and conditions outlined below."
msgstr "Merci d'utiliser Documenso pour signer vos documents électroniquement. L'objectif de cette divulgation est de vous informer sur le processus, la légalité et vos droits concernant l'utilisation des signatures électroniques sur notre plateforme. En choisissant d'utiliser une signature électronique, vous acceptez les termes et conditions énoncés ci-dessous."
msgstr "Merci d'utiliser Documenso pour signer vos documents électroniquement. L'objectif de cette clause est de vous informer sur le processus, la légalité et vos droits concernant l'utilisation de la signature électronique sur notre plateforme. En choisissant d'utiliser un sytème de signature électronique, vous acceptez les termes et conditions exposés ci-dessous."
#: packages/email/template-components/template-forgot-password.tsx:25
msgid "That's okay, it happens! Click the button below to reset your password."
msgstr "C'est d'accord, cela arrive ! Cliquez sur le bouton ci-dessous pour réinitialiser votre mot de passe."
msgstr "Ce n'est pas grave, cela arrive ! Cliquez sur le bouton ci-dessous pour réinitialiser votre mot de passe."
#: apps/web/src/app/(dashboard)/admin/users/[id]/delete-user-dialog.tsx:52
msgid "The account has been deleted successfully."
@@ -5331,7 +5331,7 @@ msgstr "Le webhook a été créé avec succès."
#: apps/web/src/app/(dashboard)/documents/empty-state.tsx:25
msgid "There are no active drafts at the current moment. You can upload a document to start drafting."
msgstr "Il n'y a pas de brouillons actifs pour le moment. Vous pouvez télécharger un document pour commencer à rédiger."
msgstr "Il n'y a pas de brouillons actifs pour le moment. Vous pouvez importer un document pour commencer un brouillon."
#: apps/web/src/app/(dashboard)/documents/empty-state.tsx:20
msgid "There are no completed documents yet. Documents that you have created or received will appear here once completed."
@@ -5899,27 +5899,27 @@ msgstr "Améliorer"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:132
msgid "Upload a CSV file to create multiple documents from this template. Each row represents one document with its recipient details."
msgstr "Téléchargez un fichier CSV pour créer plusieurs documents à partir de ce modèle. Chaque ligne représente un document avec ses détails de destinataire."
msgstr "Importer un fichier CSV pour créer plusieurs documents à partir de ce modèle. Chaque ligne représente un document avec les coordonnées de son destinataire."
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:426
msgid "Upload a custom document to use instead of the template's default document"
msgstr "Téléchargez un document personnalisé à utiliser à la place du document par défaut du modèle"
msgstr "Importer un document personnalisé à utiliser à la place du modèle par défaut"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:267
msgid "Upload and Process"
msgstr "Télécharger et traiter"
msgstr "Importer et traiter"
#: apps/web/src/components/forms/avatar-image.tsx:179
msgid "Upload Avatar"
msgstr "Télécharger un avatar"
msgstr "Importer un avatar"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:198
msgid "Upload CSV"
msgstr "Télécharger le CSV"
msgstr "Importer le CSV"
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:419
msgid "Upload custom document"
msgstr "Télécharger un document personnalisé"
msgstr "Importer un document personnalisé"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:529
msgid "Upload Signature"
@@ -5927,7 +5927,7 @@ msgstr "Importer une signature"
#: packages/ui/primitives/document-dropzone.tsx:70
msgid "Upload Template Document"
msgstr "Télécharger le document modèle"
msgstr "Importer le document modèle"
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/branding-preferences.tsx:256
msgid "Upload your brand logo (max 5MB, JPG, PNG, or WebP)"
@@ -5940,15 +5940,15 @@ msgstr "Téléversé par"
#: apps/web/src/components/forms/avatar-image.tsx:91
msgid "Uploaded file is too large"
msgstr "Le fichier téléchargé est trop volumineux"
msgstr "Le fichier importé est trop volumineux"
#: apps/web/src/components/forms/avatar-image.tsx:92
msgid "Uploaded file is too small"
msgstr "Le fichier téléchargé est trop petit"
msgstr "Le fichier importé est trop petit"
#: apps/web/src/components/forms/avatar-image.tsx:93
msgid "Uploaded file not an allowed file type"
msgstr "Le fichier téléchargé n'est pas un type de fichier autorisé"
msgstr "Le fichier importé n'est pas un type de fichier autorisé"
#: apps/web/src/app/(dashboard)/templates/[id]/template-page-view.tsx:175
msgid "Use"
@@ -6040,7 +6040,7 @@ msgstr "Vérifiez votre adresse e-mail pour débloquer toutes les fonctionnalit
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:60
msgid "Verify your email to upload documents."
msgstr "Vérifiez votre e-mail pour télécharger des documents."
msgstr "Vérifiez votre e-mail pour importer des documents."
#: packages/email/templates/confirm-team-email.tsx:71
msgid "Verify your team email address"
@@ -6137,11 +6137,11 @@ msgstr "Vu"
#: packages/lib/constants/recipient-roles.ts:32
msgid "Viewer"
msgstr "Visiteur"
msgstr "Lecteur"
#: packages/lib/constants/recipient-roles.ts:33
msgid "Viewers"
msgstr "Spectateurs"
msgstr "Lecteurs"
#: packages/lib/constants/recipient-roles.ts:31
msgid "Viewing"
@@ -6526,7 +6526,7 @@ msgstr "Vous êtes sur le point d'envoyer ce document aux destinataires. Êtes-v
#: apps/web/src/app/(dashboard)/settings/billing/page.tsx:80
msgid "You are currently on the <0>Free Plan</0>."
msgstr "Vous êtes actuellement sur le <0>Plan Gratuit</0>."
msgstr "Vous êtes actuellement sur l'<0>Abonnement Gratuit</0>."
#: apps/web/src/components/(teams)/dialogs/update-team-member-dialog.tsx:148
msgid "You are currently updating <0>{teamMemberName}.</0>"
@@ -6610,11 +6610,11 @@ msgstr "Vous ne pouvez pas modifier un membre de l'équipe qui a un rôle plus
#: packages/ui/primitives/document-dropzone.tsx:43
msgid "You cannot upload documents at this time."
msgstr "Vous ne pouvez pas télécharger de documents pour le moment."
msgstr "Vous ne pouvez pas importer de documents pour le moment."
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:103
msgid "You cannot upload encrypted PDFs"
msgstr "Vous ne pouvez pas télécharger de PDF cryptés"
msgstr "Vous ne pouvez pas importer de PDF cryptés"
#: apps/web/src/app/(dashboard)/settings/billing/billing-portal-button.tsx:46
msgid "You do not currently have a customer record, this should not happen. Please contact support for assistance."
@@ -6678,11 +6678,11 @@ msgstr "Vous n'avez pas encore de webhooks. Vos webhooks seront affichés ici un
#: apps/web/src/app/(dashboard)/templates/empty-state.tsx:15
msgid "You have not yet created any templates. To create a template please upload one."
msgstr "Vous n'avez pas encore créé de modèles. Pour créer un modèle, veuillez en télécharger un."
msgstr "Vous n'avez pas encore créé de modèles. Pour créer un modèle, veuillez en importer un."
#: apps/web/src/app/(dashboard)/documents/empty-state.tsx:30
msgid "You have not yet created or received any documents. To create a document please upload one."
msgstr "Vous n'avez pas encore créé ou reçu de documents. Pour créer un document, veuillez en télécharger un."
msgstr "Vous n'avez pas encore créé ou reçu de documents. Pour créer un document, veuillez en importer un."
#: apps/web/src/app/(dashboard)/templates/template-direct-link-dialog.tsx:229
msgid "You have reached the maximum limit of {0} direct templates. <0>Upgrade your account to continue!</0>"
@@ -6690,7 +6690,7 @@ msgstr "Vous avez atteint la limite maximale de {0} modèles directs. <0>Mettez
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:106
msgid "You have reached your document limit for this month. Please upgrade your plan."
msgstr "Vous avez atteint votre limite de documents pour ce mois. Veuillez passer à un plan supérieur."
msgstr "Vous avez atteint votre limite de documents pour ce mois. Veuillez passer à l'abonnement supérieur."
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:56
#: packages/ui/primitives/document-dropzone.tsx:69
@@ -6811,11 +6811,11 @@ msgstr "Votre envoi groupé a été initié. Vous recevrez une notification par
#: packages/email/templates/bulk-send-complete.tsx:40
msgid "Your bulk send operation for template \"{templateName}\" has completed."
msgstr "Votre opération d'envoi groupé pour le modèle \"{templateName}\" est terminée."
msgstr "Votre envoi groupé pour le modèle \"{templateName}\" est terminé."
#: apps/web/src/app/(dashboard)/settings/billing/page.tsx:125
msgid "Your current plan is past due. Please update your payment information."
msgstr "Votre plan actuel est en retard. Veuillez mettre à jour vos informations de paiement."
msgstr "Votre abonnement actuel est arrivé à échéance. Veuillez mettre à jour vos informations de paiement."
#: apps/web/src/components/templates/manage-public-template-dialog.tsx:249
msgid "Your direct signing templates"
@@ -6823,7 +6823,7 @@ msgstr "Vos modèles de signature directe"
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:123
msgid "Your document failed to upload."
msgstr "Votre document a échoué à se télécharger."
msgstr "L'importation de votre document a échoué."
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:169
msgid "Your document has been created from the template successfully."
@@ -6847,11 +6847,11 @@ msgstr "Votre document a été dupliqué avec succès."
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:86
msgid "Your document has been uploaded successfully."
msgstr "Votre document a été téléchargé avec succès."
msgstr "Votre document a été importé avec succès."
#: apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx:68
msgid "Your document has been uploaded successfully. You will be redirected to the template page."
msgstr "Votre document a été téléchargé avec succès. Vous serez redirigé vers la page de modèle."
msgstr "Votre document a été importé avec succès. Vous serez redirigé vers la page de modèle."
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:104
msgid "Your document preferences have been updated"
+15 -1
View File
@@ -29,7 +29,9 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
'DOCUMENT_FIELD_INSERTED', // When a field is inserted (signed/approved/etc) by a recipient.
'DOCUMENT_FIELD_UNINSERTED', // When a field is uninserted by a recipient.
'DOCUMENT_FIELD_PREFILLED', // When a field is prefilled by an assistant.
'DOCUMENT_VISIBILITY_UPDATED', // When the document visibility scope is updated
'DOCUMENT_VISIBILITY_UPDATED', // When the document visibility scope is updated.
'DOCUMENT_SIGNING_CERTIFICATE_UPDATED', // When the include signing certificate is updated.
'DOCUMENT_AUDIT_TRAIL_UPDATED', // When the include audit trail is updated.
'DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED', // When the global access authentication is updated.
'DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED', // When the global action authentication is updated.
'DOCUMENT_META_UPDATED', // When the document meta data is updated.
@@ -397,6 +399,16 @@ export const ZDocumentAuditLogEventDocumentVisibilitySchema = z.object({
data: ZGenericFromToSchema,
});
export const ZDocumentAuditLogEventDocumentSigningCertificateUpdatedSchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_CERTIFICATE_UPDATED),
data: ZGenericFromToSchema,
});
export const ZDocumentAuditLogEventDocumentAuditTrailUpdatedSchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_AUDIT_TRAIL_UPDATED),
data: ZGenericFromToSchema,
});
/**
* Event: Document global authentication access updated.
*/
@@ -574,6 +586,8 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
ZDocumentAuditLogEventDocumentFieldUninsertedSchema,
ZDocumentAuditLogEventDocumentFieldPrefilledSchema,
ZDocumentAuditLogEventDocumentVisibilitySchema,
ZDocumentAuditLogEventDocumentSigningCertificateUpdatedSchema,
ZDocumentAuditLogEventDocumentAuditTrailUpdatedSchema,
ZDocumentAuditLogEventDocumentGlobalAuthAccessUpdatedSchema,
ZDocumentAuditLogEventDocumentGlobalAuthActionUpdatedSchema,
ZDocumentAuditLogEventDocumentMetaUpdatedSchema,
+6
View File
@@ -18,6 +18,8 @@ import { ZRecipientLiteSchema } from './recipient';
*/
export const ZDocumentSchema = DocumentSchema.pick({
visibility: true,
includeSigningCertificate: true,
includeAuditTrailLog: true,
status: true,
source: true,
id: true,
@@ -82,6 +84,8 @@ export const ZDocumentLiteSchema = DocumentSchema.pick({
deletedAt: true,
teamId: true,
templateId: true,
includeSigningCertificate: true,
includeAuditTrailLog: true,
});
/**
@@ -104,6 +108,8 @@ export const ZDocumentManySchema = DocumentSchema.pick({
deletedAt: true,
teamId: true,
templateId: true,
includeSigningCertificate: true,
includeAuditTrailLog: true,
}).extend({
user: UserSchema.pick({
id: true,
@@ -322,6 +322,14 @@ export const formatDocumentAuditLogAction = (
anonymous: msg`Document visibility updated`,
identified: msg`${prefix} updated the document visibility`,
}))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_CERTIFICATE_UPDATED }, () => ({
anonymous: msg`Document signing certificate updated`,
identified: msg`${prefix} updated the document signing certificate`,
}))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_AUDIT_TRAIL_UPDATED }, () => ({
anonymous: msg`Document audit trail updated`,
identified: msg`${prefix} updated the document audit trail`,
}))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED }, () => ({
anonymous: msg`Document access auth updated`,
identified: msg`${prefix} updated the document access auth requirements`,
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "TeamGlobalSettings" ADD COLUMN "includeAuditTrailLog" BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Document" ADD COLUMN "includeAuditTrail" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "includeSigningCertificate" BOOLEAN NOT NULL DEFAULT true;
@@ -0,0 +1,9 @@
/*
Warnings:
- You are about to drop the column `includeAuditTrail` on the `Document` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Document" DROP COLUMN "includeAuditTrail",
ADD COLUMN "includeAuditTrailLog" BOOLEAN NOT NULL DEFAULT false;
+27 -24
View File
@@ -311,30 +311,32 @@ enum DocumentVisibility {
/// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';", "import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';"])
model Document {
id Int @id @default(autoincrement())
externalId String? /// @zod.string.describe("A custom external ID you can use to identify the document.")
userId Int /// @zod.number.describe("The ID of the user that created this document.")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema)
formValues Json? /// [DocumentFormValues] @zod.custom.use(ZDocumentFormValuesSchema)
visibility DocumentVisibility @default(EVERYONE)
title String
status DocumentStatus @default(DRAFT)
recipients Recipient[]
fields Field[]
shareLinks DocumentShareLink[]
documentDataId String
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
documentMeta DocumentMeta?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
completedAt DateTime?
deletedAt DateTime?
teamId Int?
team Team? @relation(fields: [teamId], references: [id])
templateId Int?
template Template? @relation(fields: [templateId], references: [id], onDelete: SetNull)
source DocumentSource
id Int @id @default(autoincrement())
externalId String? /// @zod.string.describe("A custom external ID you can use to identify the document.")
userId Int /// @zod.number.describe("The ID of the user that created this document.")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema)
formValues Json? /// [DocumentFormValues] @zod.custom.use(ZDocumentFormValuesSchema)
visibility DocumentVisibility @default(EVERYONE)
includeSigningCertificate Boolean @default(true)
includeAuditTrailLog Boolean @default(false)
title String
status DocumentStatus @default(DRAFT)
recipients Recipient[]
fields Field[]
shareLinks DocumentShareLink[]
documentDataId String
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
documentMeta DocumentMeta?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
completedAt DateTime?
deletedAt DateTime?
teamId Int?
team Team? @relation(fields: [teamId], references: [id])
templateId Int?
template Template? @relation(fields: [templateId], references: [id], onDelete: SetNull)
source DocumentSource
auditLogs DocumentAuditLog[]
@@ -543,6 +545,7 @@ model TeamGlobalSettings {
includeSenderDetails Boolean @default(true)
typedSignatureEnabled Boolean @default(true)
includeSigningCertificate Boolean @default(true)
includeAuditTrailLog Boolean @default(false)
brandingEnabled Boolean @default(false)
brandingLogo String @default("")
@@ -65,7 +65,7 @@ export const documentRouter = router({
.input(ZGetDocumentByIdQuerySchema)
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId } = input;
const { documentId, includeCertificate, includeAuditLog } = input;
return await getDocumentById({
userId: ctx.user.id,
@@ -63,6 +63,16 @@ export const ZDocumentVisibilitySchema = z
.nativeEnum(DocumentVisibility)
.describe('The visibility of the document.');
export const ZDocumentIncludeSigningCertificateSchema = z
.boolean()
.default(true)
.describe('Whether to include a signing certificate in the document.');
export const ZDocumentIncludeAuditTrailSchema = z
.boolean()
.default(true)
.describe('Whether to include an audit trail in the document.');
export const ZDocumentMetaTimezoneSchema = z
.string()
.describe(
@@ -141,6 +151,8 @@ export const ZFindDocumentAuditLogsQuerySchema = ZFindSearchParamsSchema.extend(
export const ZGetDocumentByIdQuerySchema = z.object({
documentId: z.number(),
includeCertificate: z.boolean().default(true).optional(),
includeAuditLog: z.boolean().default(true).optional(),
});
export const ZDuplicateDocumentRequestSchema = z.object({
@@ -235,6 +247,8 @@ export const ZUpdateDocumentRequestSchema = z.object({
title: ZDocumentTitleSchema.optional(),
externalId: ZDocumentExternalIdSchema.nullish(),
visibility: ZDocumentVisibilitySchema.optional(),
includeSigningCertificate: ZDocumentIncludeSigningCertificateSchema.optional(),
includeAuditTrailLog: ZDocumentIncludeAuditTrailSchema.optional(),
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullish(),
globalActionAuth: ZDocumentActionAuthTypesSchema.nullish(),
})
@@ -206,6 +206,7 @@ export const ZUpdateTeamDocumentSettingsMutationSchema = z.object({
includeSenderDetails: z.boolean().optional().default(false),
typedSignatureEnabled: z.boolean().optional().default(true),
includeSigningCertificate: z.boolean().optional().default(true),
includeAuditTrailLog: z.boolean().optional().default(true),
}),
});
@@ -42,6 +42,7 @@ import {
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Checkbox } from '../checkbox';
import { Combobox } from '../combobox';
import { Input } from '../input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select';
@@ -92,6 +93,8 @@ export const AddSettingsFormPartial = ({
visibility: document.visibility || '',
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
includeSigningCertificate: document.includeSigningCertificate ?? true,
includeAuditTrailLog: document.includeAuditTrailLog ?? true,
meta: {
timezone:
TIME_ZONES.find((timezone) => timezone === document.documentMeta?.timezone) ??
@@ -259,6 +262,111 @@ export const AddSettingsFormPartial = ({
/>
)}
<FormField
control={form.control}
name="globalActionAuth"
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
<Trans>Recipient action authentication</Trans>
<DocumentGlobalAuthActionTooltip />
</FormLabel>
<FormControl>
<DocumentGlobalAuthActionSelect {...field} onValueChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<Accordion type="multiple" className="mt-6">
<AccordionItem value="advanced-options" className="border-none">
<AccordionTrigger className="text-foreground mb-2 rounded border px-3 py-2 text-left hover:bg-neutral-200/30 hover:no-underline">
<Trans>Certificates</Trans>
</AccordionTrigger>
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-2 text-sm leading-relaxed">
<div className="flex flex-col space-y-6">
<FormField
control={form.control}
name="includeSigningCertificate"
render={({ field }) => (
<FormItem>
<div className="flex flex-row items-center gap-4">
<FormControl>
<Checkbox
checked={field.value}
className="h-5 w-5"
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="m-0 flex flex-row items-center">
<Trans>Include signing certificate</Trans>{' '}
<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
<Trans>
Including the signing certificate means that the certificate
will be attached to the document. You won't be able to remove
it. <br />
<br />
If you don't include it, you can download it individually.
</Trans>
</TooltipContent>
</Tooltip>
</FormLabel>
</div>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="includeAuditTrailLog"
render={({ field }) => (
<FormItem>
<div className="flex flex-row items-center gap-4">
<FormControl>
<Checkbox
checked={field.value}
className="h-5 w-5"
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="m-0 flex flex-row items-center">
<Trans>Include audit trail</Trans>{' '}
<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
<Trans>
Including the audit trail means that the log of all actions will
be attached to the document. You won't be able to remove it.{' '}
<br />
<br />
If you don't include it, you can download it individually.
</Trans>
</TooltipContent>
</Tooltip>
</FormLabel>
</div>
<FormMessage />
</FormItem>
)}
/>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
{isDocumentEnterprise && (
<FormField
control={form.control}
@@ -29,6 +29,8 @@ export const ZAddSettingsFormSchema = z.object({
title: z.string().trim().min(1, { message: "Title can't be empty" }),
externalId: z.string().optional(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
includeSigningCertificate: z.boolean().default(true).optional(),
includeAuditTrailLog: z.boolean().default(true).optional(),
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
ZDocumentAccessAuthTypesSchema.optional(),
),
@@ -282,7 +282,11 @@ export const SignaturePad = ({
event.preventDefault();
}
onMouseUp(event, false);
if (isPressed) {
onMouseUp(event, true);
} else {
onMouseUp(event, false);
}
};
const onClearClick = () => {
+83
View File
@@ -0,0 +1,83 @@
'use client';
import * as React from 'react';
import { ChevronDown } from 'lucide-react';
import { cn } from '../lib/utils';
import { Button } from './button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from './dropdown-menu';
const SplitButtonContext = React.createContext<{
variant?: React.ComponentProps<typeof Button>['variant'];
size?: React.ComponentProps<typeof Button>['size'];
}>({});
const SplitButton = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & {
variant?: React.ComponentProps<typeof Button>['variant'];
size?: React.ComponentProps<typeof Button>['size'];
}
>(({ className, children, variant = 'default', size = 'default', ...props }, ref) => {
return (
<SplitButtonContext.Provider value={{ variant, size }}>
<div ref={ref} className={cn('inline-flex', className)} {...props}>
{children}
</div>
</SplitButtonContext.Provider>
);
});
SplitButton.displayName = 'SplitButton';
const SplitButtonAction = React.forwardRef<
HTMLButtonElement,
React.ButtonHTMLAttributes<HTMLButtonElement>
>(({ className, children, ...props }, ref) => {
const { variant, size } = React.useContext(SplitButtonContext);
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn('rounded-r-none border-r-0', className)}
{...props}
>
{children}
</Button>
);
});
SplitButtonAction.displayName = 'SplitButtonAction';
const SplitButtonDropdown = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ children, ...props }, ref) => {
const { variant, size } = React.useContext(SplitButtonContext);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant={variant}
size={size}
className="rounded-l-none px-2 focus-visible:ring-offset-0"
>
<ChevronDown className="h-4 w-4" />
<span className="sr-only">More options</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" {...props} ref={ref}>
{children}
</DropdownMenuContent>
</DropdownMenu>
);
},
);
SplitButtonDropdown.displayName = 'SplitButtonDropdown';
const SplitButtonDropdownItem = DropdownMenuItem;
export { SplitButton, SplitButtonAction, SplitButtonDropdown, SplitButtonDropdownItem };