feat: add certificate translations (#1440)

Add translations for audit logs and certificates.
This commit is contained in:
David Nguyen
2024-11-05 18:25:23 +09:00
committed by GitHub
parent 011dabcc04
commit cc249357b3
9 changed files with 188 additions and 158 deletions

View File

@ -143,17 +143,11 @@ export const DocumentPageViewRecentActivity = ({
))} ))}
</div> </div>
{/* Todo: Translations. */}
<p <p
className="text-muted-foreground dark:text-muted-foreground/70 flex-auto truncate py-0.5 text-xs leading-5" className="text-muted-foreground dark:text-muted-foreground/70 flex-auto truncate py-0.5 text-xs leading-5"
title={`${formatDocumentAuditLogAction(auditLog, userId).prefix} ${ title={formatDocumentAuditLogAction(_, auditLog, userId).description}
formatDocumentAuditLogAction(auditLog, userId).description
}`}
> >
<span className="text-foreground font-medium"> {formatDocumentAuditLogAction(_, auditLog, userId).description}
{formatDocumentAuditLogAction(auditLog, userId).prefix}
</span>{' '}
{formatDocumentAuditLogAction(auditLog, userId).description}
</p> </p>
<time className="text-muted-foreground dark:text-muted-foreground/70 flex-none py-0.5 text-xs leading-5"> <time className="text-muted-foreground dark:text-muted-foreground/70 flex-none py-0.5 text-xs leading-5">

View File

@ -58,10 +58,6 @@ export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps
}); });
}; };
const uppercaseFistLetter = (text: string) => {
return text.charAt(0).toUpperCase() + text.slice(1);
};
const results = data ?? { const results = data ?? {
data: [], data: [],
perPage: 10, perPage: 10,
@ -103,9 +99,7 @@ export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps
{ {
header: _(msg`Action`), header: _(msg`Action`),
accessorKey: 'type', accessorKey: 'type',
cell: ({ row }) => ( cell: ({ row }) => <span>{formatDocumentAuditLogAction(_, row.original).description}</span>,
<span>{uppercaseFistLetter(formatDocumentAuditLogAction(row.original).description)}</span>
),
}, },
{ {
header: 'IP Address', header: 'IP Address',

View File

@ -1,5 +1,5 @@
'use client'; import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import type { DateTimeFormatOptions } from 'luxon'; import type { DateTimeFormatOptions } from 'luxon';
import { UAParser } from 'ua-parser-js'; import { UAParser } from 'ua-parser-js';
@ -25,7 +25,12 @@ const dateFormat: DateTimeFormatOptions = {
hourCycle: 'h12', hourCycle: 'h12',
}; };
/**
* DO NOT USE TRANS. YOU MUST USE _ FOR THIS FILE AND ALL CHILDREN COMPONENTS.
*/
export const AuditLogDataTable = ({ logs }: AuditLogDataTableProps) => { export const AuditLogDataTable = ({ logs }: AuditLogDataTableProps) => {
const { _ } = useLingui();
const parser = new UAParser(); const parser = new UAParser();
const uppercaseFistLetter = (text: string) => { const uppercaseFistLetter = (text: string) => {
@ -36,11 +41,11 @@ export const AuditLogDataTable = ({ logs }: AuditLogDataTableProps) => {
<Table overflowHidden> <Table overflowHidden>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Time</TableHead> <TableHead>{_(msg`Time`)}</TableHead>
<TableHead>User</TableHead> <TableHead>{_(msg`User`)}</TableHead>
<TableHead>Action</TableHead> <TableHead>{_(msg`Action`)}</TableHead>
<TableHead>IP Address</TableHead> <TableHead>{_(msg`IP Address`)}</TableHead>
<TableHead>Browser</TableHead> <TableHead>{_(msg`Browser`)}</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@ -74,7 +79,7 @@ export const AuditLogDataTable = ({ logs }: AuditLogDataTableProps) => {
</TableCell> </TableCell>
<TableCell> <TableCell>
{uppercaseFistLetter(formatDocumentAuditLogAction(log).description)} {uppercaseFistLetter(formatDocumentAuditLogAction(_, log).description)}
</TableCell> </TableCell>
<TableCell>{log.ipAddress}</TableCell> <TableCell>{log.ipAddress}</TableCell>

View File

@ -2,13 +2,18 @@ import React from 'react';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n'; import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { RECIPIENT_ROLES_DESCRIPTION_ENG } from '@documenso/lib/constants/recipient-roles'; import { DOCUMENT_STATUS } from '@documenso/lib/constants/document';
import { APP_I18N_OPTIONS, ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document'; import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt'; import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-document-audit-logs'; import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-document-audit-logs';
import { dynamicActivate } from '@documenso/lib/utils/i18n';
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { Card, CardContent } from '@documenso/ui/primitives/card';
import { Logo } from '~/components/branding/logo'; import { Logo } from '~/components/branding/logo';
@ -21,7 +26,17 @@ type AuditLogProps = {
}; };
}; };
/**
* DO NOT USE TRANS. YOU MUST USE _ FOR THIS FILE AND ALL CHILDREN COMPONENTS.
*
* Cannot use dynamicActivate by itself to translate this specific page and all
* children components because `not-found.tsx` page runs and overrides the i18n.
*/
export default async function AuditLog({ searchParams }: AuditLogProps) { export default async function AuditLog({ searchParams }: AuditLogProps) {
const { i18n } = await setupI18nSSR();
const { _ } = useLingui();
const { d } = searchParams; const { d } = searchParams;
if (typeof d !== 'string' || !d) { if (typeof d !== 'string' || !d) {
@ -44,6 +59,10 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
return redirect('/'); return redirect('/');
} }
const documentLanguage = ZSupportedLanguageCodeSchema.parse(document.documentMeta?.language);
await dynamicActivate(i18n, documentLanguage);
const { data: auditLogs } = await findDocumentAuditLogs({ const { data: auditLogs } = await findDocumentAuditLogs({
documentId: documentId, documentId: documentId,
userId: document.userId, userId: document.userId,
@ -53,31 +72,35 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
return ( return (
<div className="print-provider pointer-events-none mx-auto max-w-screen-md"> <div className="print-provider pointer-events-none mx-auto max-w-screen-md">
<div className="flex items-center"> <div className="flex items-center">
<h1 className="my-8 text-2xl font-bold">Version History</h1> <h1 className="my-8 text-2xl font-bold">{_(msg`Version History`)}</h1>
</div> </div>
<Card> <Card>
<CardContent className="grid grid-cols-2 gap-4 p-6 text-sm print:text-xs"> <CardContent className="grid grid-cols-2 gap-4 p-6 text-sm print:text-xs">
<p> <p>
<span className="font-medium">Document ID</span> <span className="font-medium">{_(msg`Document ID`)}</span>
<span className="mt-1 block break-words">{document.id}</span> <span className="mt-1 block break-words">{document.id}</span>
</p> </p>
<p> <p>
<span className="font-medium">Enclosed Document</span> <span className="font-medium">{_(msg`Enclosed Document`)}</span>
<span className="mt-1 block break-words">{document.title}</span> <span className="mt-1 block break-words">{document.title}</span>
</p> </p>
<p> <p>
<span className="font-medium">Status</span> <span className="font-medium">{_(msg`Status`)}</span>
<span className="mt-1 block">{document.deletedAt ? 'DELETED' : document.status}</span> <span className="mt-1 block">
{_(
document.deletedAt ? msg`Deleted` : DOCUMENT_STATUS[document.status].description,
).toUpperCase()}
</span>
</p> </p>
<p> <p>
<span className="font-medium">Owner</span> <span className="font-medium">{_(msg`Owner`)}</span>
<span className="mt-1 block break-words"> <span className="mt-1 block break-words">
{document.User.name} ({document.User.email}) {document.User.name} ({document.User.email})
@ -85,7 +108,7 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
</p> </p>
<p> <p>
<span className="font-medium">Created At</span> <span className="font-medium">{_(msg`Created At`)}</span>
<span className="mt-1 block"> <span className="mt-1 block">
{DateTime.fromJSDate(document.createdAt) {DateTime.fromJSDate(document.createdAt)
@ -95,7 +118,7 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
</p> </p>
<p> <p>
<span className="font-medium">Last Updated</span> <span className="font-medium">{_(msg`Last Updated`)}</span>
<span className="mt-1 block"> <span className="mt-1 block">
{DateTime.fromJSDate(document.updatedAt) {DateTime.fromJSDate(document.updatedAt)
@ -105,7 +128,7 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
</p> </p>
<p> <p>
<span className="font-medium">Time Zone</span> <span className="font-medium">{_(msg`Time Zone`)}</span>
<span className="mt-1 block break-words"> <span className="mt-1 block break-words">
{document.documentMeta?.timezone ?? 'N/A'} {document.documentMeta?.timezone ?? 'N/A'}
@ -113,13 +136,13 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
</p> </p>
<div> <div>
<p className="font-medium">Recipients</p> <p className="font-medium">{_(msg`Recipients`)}</p>
<ul className="mt-1 list-inside list-disc"> <ul className="mt-1 list-inside list-disc">
{document.Recipient.map((recipient) => ( {document.Recipient.map((recipient) => (
<li key={recipient.id}> <li key={recipient.id}>
<span className="text-muted-foreground"> <span className="text-muted-foreground">
[{RECIPIENT_ROLES_DESCRIPTION_ENG[recipient.role].roleName}] [{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}]
</span>{' '} </span>{' '}
{recipient.name} ({recipient.email}) {recipient.name} ({recipient.email})
</li> </li>

View File

@ -2,20 +2,24 @@ import React from 'react';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { UAParser } from 'ua-parser-js'; import { UAParser } from 'ua-parser-js';
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n'; import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { APP_I18N_OPTIONS, ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n';
import { import {
RECIPIENT_ROLES_DESCRIPTION_ENG, RECIPIENT_ROLES_DESCRIPTION,
RECIPIENT_ROLE_SIGNING_REASONS_ENG, RECIPIENT_ROLE_SIGNING_REASONS,
} from '@documenso/lib/constants/recipient-roles'; } from '@documenso/lib/constants/recipient-roles';
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document'; import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt'; import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
import { getDocumentCertificateAuditLogs } from '@documenso/lib/server-only/document/get-document-certificate-audit-logs'; import { getDocumentCertificateAuditLogs } from '@documenso/lib/server-only/document/get-document-certificate-audit-logs';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth'; import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { dynamicActivate } from '@documenso/lib/utils/i18n';
import { FieldType } from '@documenso/prisma/client'; import { FieldType } from '@documenso/prisma/client';
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { Card, CardContent } from '@documenso/ui/primitives/card';
import { import {
@ -36,11 +40,21 @@ type SigningCertificateProps = {
}; };
const FRIENDLY_SIGNING_REASONS = { const FRIENDLY_SIGNING_REASONS = {
['__OWNER__']: `I am the owner of this document`, ['__OWNER__']: msg`I am the owner of this document`,
...RECIPIENT_ROLE_SIGNING_REASONS_ENG, ...RECIPIENT_ROLE_SIGNING_REASONS,
}; };
/**
* DO NOT USE TRANS. YOU MUST USE _ FOR THIS FILE AND ALL CHILDREN COMPONENTS.
*
* Cannot use dynamicActivate by itself to translate this specific page and all
* children components because `not-found.tsx` page runs and overrides the i18n.
*/
export default async function SigningCertificate({ searchParams }: SigningCertificateProps) { export default async function SigningCertificate({ searchParams }: SigningCertificateProps) {
const { i18n } = await setupI18nSSR();
const { _ } = useLingui();
const { d } = searchParams; const { d } = searchParams;
if (typeof d !== 'string' || !d) { if (typeof d !== 'string' || !d) {
@ -63,6 +77,10 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
return redirect('/'); return redirect('/');
} }
const documentLanguage = ZSupportedLanguageCodeSchema.parse(document.documentMeta?.language);
await dynamicActivate(i18n, documentLanguage);
const auditLogs = await getDocumentCertificateAuditLogs({ const auditLogs = await getDocumentCertificateAuditLogs({
id: documentId, id: documentId,
}); });
@ -98,17 +116,17 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
}); });
let authLevel = match(extractedAuthMethods.derivedRecipientActionAuth) let authLevel = match(extractedAuthMethods.derivedRecipientActionAuth)
.with('ACCOUNT', () => 'Account Re-Authentication') .with('ACCOUNT', () => _(msg`Account Re-Authentication`))
.with('TWO_FACTOR_AUTH', () => 'Two-Factor Re-Authentication') .with('TWO_FACTOR_AUTH', () => _(msg`Two-Factor Re-Authentication`))
.with('PASSKEY', () => 'Passkey Re-Authentication') .with('PASSKEY', () => _(msg`Passkey Re-Authentication`))
.with('EXPLICIT_NONE', () => 'Email') .with('EXPLICIT_NONE', () => _(msg`Email`))
.with(null, () => null) .with(null, () => null)
.exhaustive(); .exhaustive();
if (!authLevel) { if (!authLevel) {
authLevel = match(extractedAuthMethods.derivedRecipientAccessAuth) authLevel = match(extractedAuthMethods.derivedRecipientAccessAuth)
.with('ACCOUNT', () => 'Account Authentication') .with('ACCOUNT', () => _(msg`Account Authentication`))
.with(null, () => 'Email') .with(null, () => _(msg`Email`))
.exhaustive(); .exhaustive();
} }
@ -147,7 +165,7 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
return ( return (
<div className="print-provider pointer-events-none mx-auto max-w-screen-md"> <div className="print-provider pointer-events-none mx-auto max-w-screen-md">
<div className="flex items-center"> <div className="flex items-center">
<h1 className="my-8 text-2xl font-bold">Signing Certificate</h1> <h1 className="my-8 text-2xl font-bold">{_(msg`Signing Certificate`)}</h1>
</div> </div>
<Card> <Card>
@ -155,9 +173,9 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
<Table overflowHidden> <Table overflowHidden>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Signer Events</TableHead> <TableHead>{_(msg`Signer Events`)}</TableHead>
<TableHead>Signature</TableHead> <TableHead>{_(msg`Signature`)}</TableHead>
<TableHead>Details</TableHead> <TableHead>{_(msg`Details`)}</TableHead>
{/* <TableHead>Security</TableHead> */} {/* <TableHead>Security</TableHead> */}
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@ -173,11 +191,11 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
<div className="hyphens-auto break-words font-medium">{recipient.name}</div> <div className="hyphens-auto break-words font-medium">{recipient.name}</div>
<div className="break-all">{recipient.email}</div> <div className="break-all">{recipient.email}</div>
<p className="text-muted-foreground mt-2 text-sm print:text-xs"> <p className="text-muted-foreground mt-2 text-sm print:text-xs">
{RECIPIENT_ROLES_DESCRIPTION_ENG[recipient.role].roleName} {_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
</p> </p>
<p className="text-muted-foreground mt-2 text-sm print:text-xs"> <p className="text-muted-foreground mt-2 text-sm print:text-xs">
<span className="font-medium">Authentication Level:</span>{' '} <span className="font-medium">{_(msg`Authentication Level`)}:</span>{' '}
<span className="block">{getAuthenticationLevel(recipient.id)}</span> <span className="block">{getAuthenticationLevel(recipient.id)}</span>
</p> </p>
</TableCell> </TableCell>
@ -199,21 +217,21 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
</div> </div>
<p className="text-muted-foreground mt-2 text-sm print:text-xs"> <p className="text-muted-foreground mt-2 text-sm print:text-xs">
<span className="font-medium">Signature ID:</span>{' '} <span className="font-medium">{_(msg`Signature ID`)}:</span>{' '}
<span className="block font-mono uppercase"> <span className="block font-mono uppercase">
{signature.secondaryId} {signature.secondaryId}
</span> </span>
</p> </p>
<p className="text-muted-foreground mt-2 text-sm print:text-xs"> <p className="text-muted-foreground mt-2 text-sm print:text-xs">
<span className="font-medium">IP Address:</span>{' '} <span className="font-medium">{_(msg`IP Address`)}:</span>{' '}
<span className="inline-block"> <span className="inline-block">
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.ipAddress ?? 'Unknown'} {logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.ipAddress ?? _(msg`Unknown`)}
</span> </span>
</p> </p>
<p className="text-muted-foreground mt-1 text-sm print:text-xs"> <p className="text-muted-foreground mt-1 text-sm print:text-xs">
<span className="font-medium">Device:</span>{' '} <span className="font-medium">{_(msg`Device`)}:</span>{' '}
<span className="inline-block"> <span className="inline-block">
{getDevice(logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.userAgent)} {getDevice(logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.userAgent)}
</span> </span>
@ -227,44 +245,46 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
<TableCell truncate={false} className="w-[min-content] align-top"> <TableCell truncate={false} className="w-[min-content] align-top">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-muted-foreground text-sm print:text-xs"> <p className="text-muted-foreground text-sm print:text-xs">
<span className="font-medium">Sent:</span>{' '} <span className="font-medium">{_(msg`Sent`)}:</span>{' '}
<span className="inline-block"> <span className="inline-block">
{logs.EMAIL_SENT[0] {logs.EMAIL_SENT[0]
? DateTime.fromJSDate(logs.EMAIL_SENT[0].createdAt) ? DateTime.fromJSDate(logs.EMAIL_SENT[0].createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale) .setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)') .toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
: 'Unknown'} : _(msg`Unknown`)}
</span> </span>
</p> </p>
<p className="text-muted-foreground text-sm print:text-xs"> <p className="text-muted-foreground text-sm print:text-xs">
<span className="font-medium">Viewed:</span>{' '} <span className="font-medium">{_(msg`Viewed`)}:</span>{' '}
<span className="inline-block"> <span className="inline-block">
{logs.DOCUMENT_OPENED[0] {logs.DOCUMENT_OPENED[0]
? DateTime.fromJSDate(logs.DOCUMENT_OPENED[0].createdAt) ? DateTime.fromJSDate(logs.DOCUMENT_OPENED[0].createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale) .setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)') .toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
: 'Unknown'} : _(msg`Unknown`)}
</span> </span>
</p> </p>
<p className="text-muted-foreground text-sm print:text-xs"> <p className="text-muted-foreground text-sm print:text-xs">
<span className="font-medium">Signed:</span>{' '} <span className="font-medium">{_(msg`Signed`)}:</span>{' '}
<span className="inline-block"> <span className="inline-block">
{logs.DOCUMENT_RECIPIENT_COMPLETED[0] {logs.DOCUMENT_RECIPIENT_COMPLETED[0]
? DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt) ? DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale) .setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)') .toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
: 'Unknown'} : _(msg`Unknown`)}
</span> </span>
</p> </p>
<p className="text-muted-foreground text-sm print:text-xs"> <p className="text-muted-foreground text-sm print:text-xs">
<span className="font-medium">Reason:</span>{' '} <span className="font-medium">{_(msg`Reason`)}:</span>{' '}
<span className="inline-block"> <span className="inline-block">
{isOwner(recipient.email) {_(
? FRIENDLY_SIGNING_REASONS['__OWNER__'] isOwner(recipient.email)
: FRIENDLY_SIGNING_REASONS[recipient.role]} ? FRIENDLY_SIGNING_REASONS['__OWNER__']
: FRIENDLY_SIGNING_REASONS[recipient.role],
)}
</span> </span>
</p> </p>
</div> </div>
@ -280,7 +300,7 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
<div className="my-8 flex-row-reverse"> <div className="my-8 flex-row-reverse">
<div className="flex items-end justify-end gap-x-4"> <div className="flex items-end justify-end gap-x-4">
<p className="flex-shrink-0 text-sm font-medium print:text-xs"> <p className="flex-shrink-0 text-sm font-medium print:text-xs">
Signing certificate provided by: {_(msg`Signing certificate provided by`)}:
</p> </p>
<Logo className="max-h-6 print:max-h-4" /> <Logo className="max-h-6 print:max-h-4" />

View File

@ -12,7 +12,7 @@ import { UAParser } from 'ua-parser-js';
import { DOCUMENT_AUDIT_LOG_EMAIL_FORMAT } from '@documenso/lib/constants/document-audit-logs'; import { DOCUMENT_AUDIT_LOG_EMAIL_FORMAT } from '@documenso/lib/constants/document-audit-logs';
import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth'; import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import { formatDocumentAuditLogActionString } from '@documenso/lib/utils/document-audit-logs'; import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
@ -37,7 +37,7 @@ export const DocumentHistorySheet = ({
onMenuOpenChange, onMenuOpenChange,
children, children,
}: DocumentHistorySheetProps) => { }: DocumentHistorySheetProps) => {
const { i18n } = useLingui(); const { _, i18n } = useLingui();
const [isUserDetailsVisible, setIsUserDetailsVisible] = useState(false); const [isUserDetailsVisible, setIsUserDetailsVisible] = useState(false);
@ -152,7 +152,7 @@ export const DocumentHistorySheet = ({
<div> <div>
<p className="text-foreground text-xs font-bold"> <p className="text-foreground text-xs font-bold">
{formatDocumentAuditLogActionString(auditLog, userId)} {formatDocumentAuditLogAction(_, auditLog, userId).description}
</p> </p>
<p className="text-foreground/50 text-xs"> <p className="text-foreground/50 text-xs">
{DateTime.fromJSDate(auditLog.createdAt) {DateTime.fromJSDate(auditLog.createdAt)

View File

@ -0,0 +1,18 @@
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/macro';
import { DocumentStatus } from '@documenso/prisma/client';
export const DOCUMENT_STATUS: {
[status in DocumentStatus]: { description: MessageDescriptor };
} = {
[DocumentStatus.COMPLETED]: {
description: msg`Completed`,
},
[DocumentStatus.DRAFT]: {
description: msg`Draft`,
},
[DocumentStatus.PENDING]: {
description: msg`Pending`,
},
};

View File

@ -78,13 +78,3 @@ export const RECIPIENT_ROLE_SIGNING_REASONS = {
[RecipientRole.CC]: msg`I am required to receive a copy of this document`, [RecipientRole.CC]: msg`I am required to receive a copy of this document`,
[RecipientRole.VIEWER]: msg`I am a viewer of this document`, [RecipientRole.VIEWER]: msg`I am a viewer of this document`,
} satisfies Record<keyof typeof RecipientRole, MessageDescriptor>; } satisfies Record<keyof typeof RecipientRole, MessageDescriptor>;
/**
* Raw english descriptions for certificates.
*/
export const RECIPIENT_ROLE_SIGNING_REASONS_ENG = {
[RecipientRole.SIGNER]: `I am a signer of this document`,
[RecipientRole.APPROVER]: `I am an approver of this document`,
[RecipientRole.CC]: `I am required to receive a copy of this document`,
[RecipientRole.VIEWER]: `I am a viewer of this document`,
} satisfies Record<keyof typeof RecipientRole, string>;

View File

@ -1,14 +1,10 @@
import type { I18n } from '@lingui/core';
import { msg } from '@lingui/macro';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import type { import type { DocumentAuditLog, DocumentMeta, Field, Recipient } from '@documenso/prisma/client';
DocumentAuditLog, import { RecipientRole } from '@documenso/prisma/client';
DocumentMeta,
Field,
Recipient,
RecipientRole,
} from '@documenso/prisma/client';
import { RECIPIENT_ROLES_DESCRIPTION_ENG } from '../constants/recipient-roles';
import type { import type {
TDocumentAuditLog, TDocumentAuditLog,
TDocumentAuditLogDocumentMetaDiffSchema, TDocumentAuditLogDocumentMetaDiffSchema,
@ -254,129 +250,119 @@ export const diffDocumentMetaChanges = (
* *
* Provide a userId to prefix the action with the user, example 'X did Y'. * Provide a userId to prefix the action with the user, example 'X did Y'.
*/ */
export const formatDocumentAuditLogActionString = ( export const formatDocumentAuditLogAction = (
_: I18n['_'],
auditLog: TDocumentAuditLog, auditLog: TDocumentAuditLog,
userId?: number, userId?: number,
) => { ) => {
const { prefix, description } = formatDocumentAuditLogAction(auditLog, userId); const prefix = userId === auditLog.userId ? _(msg`You`) : auditLog.name || auditLog.email || '';
return prefix ? `${prefix} ${description}` : description;
};
/**
* Formats the audit log into a description of the action.
*
* Provide a userId to prefix the action with the user, example 'X did Y'.
*/
// Todo: Translations.
export const formatDocumentAuditLogAction = (auditLog: TDocumentAuditLog, userId?: number) => {
let prefix = userId === auditLog.userId ? 'You' : auditLog.name || auditLog.email || '';
const description = match(auditLog) const description = match(auditLog)
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED }, () => ({
anonymous: 'A field was added', anonymous: msg`A field was added`,
identified: 'added a field', identified: msg`${prefix} added a field`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED }, () => ({
anonymous: 'A field was removed', anonymous: msg`A field was removed`,
identified: 'removed a field', identified: msg`${prefix} removed a field`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED }, () => ({
anonymous: 'A field was updated', anonymous: msg`A field was updated`,
identified: 'updated a field', identified: msg`${prefix} updated a field`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED }, () => ({
anonymous: 'A recipient was added', anonymous: msg`A recipient was added`,
identified: 'added a recipient', identified: msg`${prefix} added a recipient`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED }, () => ({
anonymous: 'A recipient was removed', anonymous: msg`A recipient was removed`,
identified: 'removed a recipient', identified: msg`${prefix} removed a recipient`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED }, () => ({
anonymous: 'A recipient was updated', anonymous: msg`A recipient was updated`,
identified: 'updated a recipient', identified: msg`${prefix} updated a recipient`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED }, () => ({
anonymous: 'Document created', anonymous: msg`Document created`,
identified: 'created the document', identified: msg`${prefix} created the document`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED }, () => ({
anonymous: 'Document deleted', anonymous: msg`Document deleted`,
identified: 'deleted the document', identified: msg`${prefix} deleted the document`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED }, () => ({
anonymous: 'Field signed', anonymous: msg`Field signed`,
identified: 'signed a field', identified: msg`${prefix} signed a field`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED }, () => ({
anonymous: 'Field unsigned', anonymous: msg`Field unsigned`,
identified: 'unsigned a field', identified: msg`${prefix} unsigned a field`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED }, () => ({
anonymous: 'Document visibility updated', anonymous: msg`Document visibility updated`,
identified: 'updated the document visibility', identified: msg`${prefix} updated the document visibility`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED }, () => ({
anonymous: 'Document access auth updated', anonymous: msg`Document access auth updated`,
identified: 'updated the document access auth requirements', identified: msg`${prefix} updated the document access auth requirements`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED }, () => ({
anonymous: 'Document signing auth updated', anonymous: msg`Document signing auth updated`,
identified: 'updated the document signing auth requirements', identified: msg`${prefix} updated the document signing auth requirements`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED }, () => ({
anonymous: 'Document updated', anonymous: msg`Document updated`,
identified: 'updated the document', identified: msg`${prefix} updated the document`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED }, () => ({
anonymous: 'Document opened', anonymous: msg`Document opened`,
identified: 'opened the document', identified: msg`${prefix} opened the document`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED }, () => ({
anonymous: 'Document title updated', anonymous: msg`Document title updated`,
identified: 'updated the document title', identified: msg`${prefix} updated the document title`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED }, () => ({
anonymous: 'Document external ID updated', anonymous: msg`Document external ID updated`,
identified: 'updated the document external ID', identified: msg`${prefix} updated the document external ID`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT }, () => ({
anonymous: 'Document sent', anonymous: msg`Document sent`,
identified: 'sent the document', identified: msg`${prefix} sent the document`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM }, () => ({
anonymous: 'Document moved to team', anonymous: msg`Document moved to team`,
identified: 'moved the document to team', identified: msg`${prefix} moved the document to team`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED }, ({ data }) => { .with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED }, ({ data }) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions const userName = prefix || _(msg`Recipient`);
const action = RECIPIENT_ROLES_DESCRIPTION_ENG[data.recipientRole as RecipientRole]?.actioned;
const value = action ? `${action.toLowerCase()} the document` : 'completed their task'; const result = match(data.recipientRole)
.with(RecipientRole.SIGNER, () => msg`${userName} signed the document`)
.with(RecipientRole.VIEWER, () => msg`${userName} viewed the document`)
.with(RecipientRole.APPROVER, () => msg`${userName} approved the document`)
.with(RecipientRole.CC, () => msg`${userName} CC'd the document`)
.otherwise(() => msg`${userName} completed their task`);
return { return {
anonymous: `Recipient ${value}`, anonymous: result,
identified: value, identified: result,
}; };
}) })
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT }, ({ data }) => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT }, ({ data }) => ({
anonymous: `Email ${data.isResending ? 'resent' : 'sent'}`, anonymous: data.isResending ? msg`Email resent` : msg`Email sent`,
identified: `${data.isResending ? 'resent' : 'sent'} an email to ${data.recipientEmail}`, identified: data.isResending
? msg`${prefix} resent an email to ${data.recipientEmail}`
: msg`${prefix} sent an email to ${data.recipientEmail}`,
}))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED }, () => ({
anonymous: msg`Document completed`,
identified: msg`Document completed`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED }, () => {
// Clear the prefix since this should be considered an 'anonymous' event.
prefix = '';
return {
anonymous: 'Document completed',
identified: 'Document completed',
};
})
.exhaustive(); .exhaustive();
return { return {
prefix, prefix,
description: prefix ? description.identified : description.anonymous, description: _(prefix ? description.identified : description.anonymous),
}; };
}; };