Merge branch 'main' into staging

This commit is contained in:
Timur Ercan
2024-11-07 16:10:10 +01:00
71 changed files with 3940 additions and 776 deletions

View File

@ -93,6 +93,8 @@ NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS=
NEXT_PRIVATE_SMTP_FROM_NAME="Documenso"
# REQUIRED: Defines the email address to use as the from address.
NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@documenso.com"
# OPTIONAL: Defines the service for nodemailer
NEXT_PRIVATE_SMTP_SERVICE=
# OPTIONAL: The API key to use for Resend.com
NEXT_PRIVATE_RESEND_API_KEY=
# OPTIONAL: The API key to use for MailChannels.

View File

@ -38,11 +38,17 @@ You will be prompted to enter some information, such as the certificate's Common
Combine the private key and the self-signed certificate to create a `.p12` certificate. Use the following command:
```bash
openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt
openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt -legacy
```
<Callout type="warning">
If you get the error "Error: Failed to get private key bags", add the `-legacy` flag to the command `openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt -legacy`.
When running the application in Docker, you may encounter permission issues when attempting to sign documents using your certificate (.p12) file. This happens because the application runs as a non-root user inside the container and needs read access to the certificate.
To resolve this, you'll need to update the certificate file permissions to allow the container user 1001, which runs NextJS, to read it:
```bash
sudo chown 1001 certificate.p12
```
</Callout>
@ -54,8 +60,8 @@ Note that for local development, the password can be left empty.
### Add Certificate to the Project
Finally, add the certificate to the project. Place the `certificate.p12` file in the `/apps/web/resources` directory. If the directory doesn't exist, create it.
Use the `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` environment variable to point at the certificate you created.
The final file path should be `/apps/web/resources/certificate.p12`.
Details about environment variables associated with certificates can be found [here](/developers/self-hosting/signing-certificate#configure-documenso-to-use-the-certificate).
</Steps>

View File

@ -133,7 +133,7 @@ volumes:
After updating the volume binding, save the `compose.yml` file and run the following command to start the containers:
```bash
docker-compose --env-file ./.env -d up
docker-compose --env-file ./.env up -d
```
The command will start the PostgreSQL database and the Documenso application containers.

View File

@ -1,6 +1,6 @@
{
"name": "@documenso/marketing",
"version": "1.7.2-rc.3",
"version": "1.7.2-rc.4",
"private": true,
"license": "AGPL-3.0",
"scripts": {

View File

@ -0,0 +1,23 @@
'use client';
import Image from 'next/image';
import type { DocumentTypes } from 'contentlayer/generated';
import type { MDXComponents } from 'mdx/types';
import { useMDXComponent } from 'next-contentlayer/hooks';
const mdxComponents: MDXComponents = {
MdxNextImage: (props: { width: number; height: number; alt?: string; src: string }) => (
<Image {...props} alt={props.alt ?? ''} />
),
};
export type ContentPageContentProps = {
document: DocumentTypes;
};
export const ContentPageContent = ({ document }: ContentPageContentProps) => {
const MDXContent = useMDXComponent(document.body.code);
return <MDXContent components={mdxComponents} />;
};

View File

@ -1,12 +1,11 @@
import Image from 'next/image';
import { notFound } from 'next/navigation';
import { allDocuments } from 'contentlayer/generated';
import type { MDXComponents } from 'mdx/types';
import { useMDXComponent } from 'next-contentlayer/hooks';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { ContentPageContent } from './content';
export const dynamic = 'force-dynamic';
export const generateMetadata = ({ params }: { params: { content: string } }) => {
@ -19,12 +18,6 @@ export const generateMetadata = ({ params }: { params: { content: string } }) =>
return { title: document.title };
};
const mdxComponents: MDXComponents = {
MdxNextImage: (props: { width: number; height: number; alt?: string; src: string }) => (
<Image {...props} alt={props.alt ?? ''} />
),
};
/**
* A generic catch all page for the root level that checks for content layer documents.
*
@ -39,11 +32,9 @@ export default async function ContentPage({ params }: { params: { content: strin
notFound();
}
const MDXContent = useMDXComponent(post.body.code);
return (
<article className="prose dark:prose-invert mx-auto">
<MDXContent components={mdxComponents} />
<ContentPageContent document={post} />
</article>
);
}

View File

@ -0,0 +1,23 @@
'use client';
import Image from 'next/image';
import type { BlogPost } from 'contentlayer/generated';
import type { MDXComponents } from 'mdx/types';
import { useMDXComponent } from 'next-contentlayer/hooks';
const mdxComponents: MDXComponents = {
MdxNextImage: (props: { width: number; height: number; alt?: string; src: string }) => (
<Image {...props} alt={props.alt ?? ''} />
),
};
export type BlogPostContentProps = {
post: BlogPost;
};
export const BlogPostContent = ({ post }: BlogPostContentProps) => {
const MdxContent = useMDXComponent(post.body.code);
return <MdxContent components={mdxComponents} />;
};

View File

@ -1,16 +1,15 @@
import Image from 'next/image';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { allBlogPosts } from 'contentlayer/generated';
import { ChevronLeft } from 'lucide-react';
import type { MDXComponents } from 'mdx/types';
import { useMDXComponent } from 'next-contentlayer/hooks';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { CallToAction } from '~/components/(marketing)/call-to-action';
import { BlogPostContent } from './content';
export const dynamic = 'force-dynamic';
export const generateMetadata = ({ params }: { params: { post: string } }) => {
@ -42,12 +41,6 @@ export const generateMetadata = ({ params }: { params: { post: string } }) => {
};
};
const mdxComponents: MDXComponents = {
MdxNextImage: (props: { width: number; height: number; alt?: string; src: string }) => (
<Image {...props} alt={props.alt ?? ''} />
),
};
export default async function BlogPostPage({ params }: { params: { post: string } }) {
await setupI18nSSR();
@ -57,8 +50,6 @@ export default async function BlogPostPage({ params }: { params: { post: string
notFound();
}
const MDXContent = useMDXComponent(post.body.code);
return (
<div>
<article className="prose dark:prose-invert mx-auto py-8">
@ -87,7 +78,7 @@ export default async function BlogPostPage({ params }: { params: { post: string
</div>
</div>
<MDXContent components={mdxComponents} />
<BlogPostContent post={post} />
{post.tags.length > 0 && (
<ul className="not-prose flex list-none flex-row space-x-2 px-0">

View File

@ -1,6 +1,7 @@
import type { Metadata } from 'next';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { z } from 'zod';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
@ -130,9 +131,9 @@ const fetchEarlyAdopters = async () => {
};
export default async function OpenPage() {
const { i18n } = await setupI18nSSR();
await setupI18nSSR();
const { _ } = i18n;
const { _ } = useLingui();
const [
{ forks_count: forksCount, stargazers_count: stargazersCount },

View File

@ -1,6 +1,6 @@
{
"name": "@documenso/web",
"version": "1.7.2-rc.3",
"version": "1.7.2-rc.4",
"private": true,
"license": "AGPL-3.0",
"scripts": {

View File

@ -33,6 +33,8 @@ import {
} from '@documenso/ui/primitives/dropdown-menu';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog';
import { ResendDocumentActionItem } from '../_action-items/resend-document';
import { DeleteDocumentDialog } from '../delete-document-dialog';
import { DuplicateDocumentDialog } from '../duplicate-document-dialog';
@ -62,6 +64,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
const isOwner = document.User.id === session.user.id;
const isDraft = document.status === DocumentStatus.DRAFT;
const isPending = document.status === DocumentStatus.PENDING;
const isDeleted = document.deletedAt !== null;
const isComplete = document.status === DocumentStatus.COMPLETED;
const isCurrentTeamDocument = team && document.team?.url === team.url;
@ -145,6 +148,21 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
<Trans>Share</Trans>
</DropdownMenuLabel>
{canManageDocument && (
<DocumentRecipientLinkCopyDialog
recipients={document.Recipient}
trigger={
<DropdownMenuItem
disabled={!isPending || isDeleted}
onSelect={(e) => e.preventDefault()}
>
<Copy className="mr-2 h-4 w-4" />
<Trans>Signing Links</Trans>
</DropdownMenuItem>
}
/>
)}
<ResendDocumentActionItem
document={document}
recipients={nonSignedRecipients}

View File

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

View File

@ -1,3 +1,5 @@
'use client';
import Link from 'next/link';
import { Trans, msg } from '@lingui/macro';
@ -6,11 +8,14 @@ import { CheckIcon, Clock, MailIcon, MailOpenIcon, PenIcon, PlusIcon } from 'luc
import { match } from 'ts-pattern';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { formatSigningLink } from '@documenso/lib/utils/recipients';
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import type { Document, Recipient } from '@documenso/prisma/client';
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
import { SignatureIcon } from '@documenso/ui/icons/signature';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
import { Badge } from '@documenso/ui/primitives/badge';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type DocumentPageViewRecipientsProps = {
document: Document & {
@ -24,6 +29,7 @@ export const DocumentPageViewRecipients = ({
documentRootPath,
}: DocumentPageViewRecipientsProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const recipients = document.Recipient;
@ -68,6 +74,7 @@ export const DocumentPageViewRecipients = ({
}
/>
<div className="flex flex-row items-center">
{document.status !== DocumentStatus.DRAFT &&
recipient.signingStatus === SigningStatus.SIGNED && (
<Badge variant="default">
@ -115,6 +122,21 @@ export const DocumentPageViewRecipients = ({
<Trans>Pending</Trans>
</Badge>
)}
{document.status === DocumentStatus.PENDING &&
recipient.signingStatus === SigningStatus.NOT_SIGNED &&
recipient.role !== RecipientRole.CC && (
<CopyTextButton
value={formatSigningLink(recipient.token)}
onCopySuccess={() => {
toast({
title: _(msg`Copied to clipboard`),
description: _(msg`The signing link has been copied to your clipboard.`),
});
}}
/>
)}
</div>
</li>
))}
</ul>

View File

@ -26,6 +26,7 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
import { DocumentHistorySheet } from '~/components/document/document-history-sheet';
import { DocumentReadOnlyFields } from '~/components/document/document-read-only-fields';
import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog';
import {
DocumentStatus as DocumentStatusComponent,
FRIENDLY_STATUS_MAP,
@ -134,6 +135,10 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
return (
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
{document.status === DocumentStatus.PENDING && (
<DocumentRecipientLinkCopyDialog recipients={recipients} />
)}
<Link href={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
<Trans>Documents</Trans>

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 ?? {
data: [],
perPage: 10,
@ -103,9 +99,7 @@ export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps
{
header: _(msg`Action`),
accessorKey: 'type',
cell: ({ row }) => (
<span>{uppercaseFistLetter(formatDocumentAuditLogAction(row.original).description)}</span>
),
cell: ({ row }) => <span>{formatDocumentAuditLogAction(_, row.original).description}</span>,
},
{
header: 'IP Address',

View File

@ -37,6 +37,8 @@ import {
} from '@documenso/ui/primitives/dropdown-menu';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog';
import { ResendDocumentActionItem } from './_action-items/resend-document';
import { DeleteDocumentDialog } from './delete-document-dialog';
import { DuplicateDocumentDialog } from './duplicate-document-dialog';
@ -69,7 +71,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
const isOwner = row.User.id === session.user.id;
// const isRecipient = !!recipient;
const isDraft = row.status === DocumentStatus.DRAFT;
// const isPending = row.status === DocumentStatus.PENDING;
const isPending = row.status === DocumentStatus.PENDING;
const isComplete = row.status === DocumentStatus.COMPLETED;
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
const isCurrentTeamDocument = team && row.team?.url === team.url;
@ -191,6 +193,20 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
<Trans>Share</Trans>
</DropdownMenuLabel>
{canManageDocument && (
<DocumentRecipientLinkCopyDialog
recipients={row.Recipient}
trigger={
<DropdownMenuItem disabled={!isPending} asChild onSelect={(e) => e.preventDefault()}>
<div>
<Copy className="mr-2 h-4 w-4" />
<Trans>Signing Links</Trans>
</div>
</DropdownMenuItem>
}
/>
)}
<ResendDocumentActionItem document={row} recipients={nonSignedRecipients} team={team} />
<DocumentShareButton

View File

@ -44,11 +44,11 @@ export const BillingPlans = ({ prices }: BillingPlansProps) => {
const isMounted = useIsMounted();
const [interval, setInterval] = useState<Interval>('month');
const [isFetchingCheckoutSession, setIsFetchingCheckoutSession] = useState(false);
const [checkoutSessionPriceId, setCheckoutSessionPriceId] = useState<string | null>(null);
const onSubscribeClick = async (priceId: string) => {
try {
setIsFetchingCheckoutSession(true);
setCheckoutSessionPriceId(priceId);
const url = await createCheckout({ priceId });
@ -64,7 +64,7 @@ export const BillingPlans = ({ prices }: BillingPlansProps) => {
variant: 'destructive',
});
} finally {
setIsFetchingCheckoutSession(false);
setCheckoutSessionPriceId(null);
}
};
@ -122,7 +122,8 @@ export const BillingPlans = ({ prices }: BillingPlansProps) => {
<Button
className="mt-4"
loading={isFetchingCheckoutSession}
disabled={checkoutSessionPriceId !== null}
loading={checkoutSessionPriceId === price.id}
onClick={() => void onSubscribeClick(price.id)}
>
<Trans>Subscribe</Trans>

View File

@ -0,0 +1,14 @@
import React from 'react';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import type { TemplateEditPageViewProps } from './template-edit-page-view';
import { TemplateEditPageView } from './template-edit-page-view';
type TemplateEditPageProps = Pick<TemplateEditPageViewProps, 'params'>;
export default async function TemplateEditPage({ params }: TemplateEditPageProps) {
await setupI18nSSR();
return <TemplateEditPageView params={params} />;
}

View File

@ -0,0 +1,96 @@
import React from 'react';
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { Trans } from '@lingui/macro';
import { ChevronLeft } from 'lucide-react';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getTemplateWithDetailsById } from '@documenso/lib/server-only/template/get-template-with-details-by-id';
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
import type { Team } from '@documenso/prisma/client';
import { TemplateType } from '~/components/formatter/template-type';
import { TemplateDirectLinkBadge } from '../../template-direct-link-badge';
import { TemplateDirectLinkDialogWrapper } from '../template-direct-link-dialog-wrapper';
import { EditTemplateForm } from './edit-template';
export type TemplateEditPageViewProps = {
params: {
id: string;
};
team?: Team;
};
export const TemplateEditPageView = async ({ params, team }: TemplateEditPageViewProps) => {
const { id } = params;
const templateId = Number(id);
const templateRootPath = formatTemplatesPath(team?.url);
if (!templateId || Number.isNaN(templateId)) {
redirect(templateRootPath);
}
const { user } = await getRequiredServerComponentSession();
const template = await getTemplateWithDetailsById({
id: templateId,
userId: user.id,
}).catch(() => null);
if (!template || !template.templateDocumentData) {
redirect(templateRootPath);
}
const isTemplateEnterprise = await isUserEnterprise({
userId: user.id,
teamId: team?.id,
});
return (
<div className="mx-auto -mt-4 max-w-screen-xl px-4 md:px-8">
<div className="flex flex-col justify-between sm:flex-row">
<div>
<Link
href={`${templateRootPath}/${templateId}`}
className="flex items-center text-[#7AC455] hover:opacity-80"
>
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
<Trans>Template</Trans>
</Link>
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={template.title}>
{template.title}
</h1>
<div className="mt-2.5 flex items-center">
<TemplateType inheritColor className="text-muted-foreground" type={template.type} />
{template.directLink?.token && (
<TemplateDirectLinkBadge
className="ml-4"
token={template.directLink.token}
enabled={template.directLink.enabled}
/>
)}
</div>
</div>
<div className="mt-2 sm:mt-0 sm:self-end">
<TemplateDirectLinkDialogWrapper template={template} />
</div>
</div>
<EditTemplateForm
className="mt-6"
initialTemplate={template}
templateRootPath={templateRootPath}
isEnterprise={isTemplateEnterprise}
/>
</div>
);
};

View File

@ -1,11 +1,12 @@
import React from 'react';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import type { TemplatePageViewProps } from './template-page-view';
import { TemplatePageView } from './template-page-view';
type TemplatePageProps = Pick<TemplatePageViewProps, 'params'>;
export type TemplatePageProps = {
params: {
id: string;
};
};
export default async function TemplatePage({ params }: TemplatePageProps) {
await setupI18nSSR();

View File

@ -10,11 +10,13 @@ import { Button } from '@documenso/ui/primitives/button';
import { TemplateDirectLinkDialog } from '../template-direct-link-dialog';
export type TemplatePageViewProps = {
export type TemplateDirectLinkDialogWrapperProps = {
template: Template & { directLink?: TemplateDirectLink | null; Recipient: Recipient[] };
};
export const TemplateDirectLinkDialogWrapper = ({ template }: TemplatePageViewProps) => {
export const TemplateDirectLinkDialogWrapper = ({
template,
}: TemplateDirectLinkDialogWrapperProps) => {
const [isTemplateDirectLinkOpen, setTemplateDirectLinkOpen] = useState(false);
return (

View File

@ -0,0 +1,281 @@
'use client';
import { useMemo } from 'react';
import { useSearchParams } from 'next/navigation';
import type { MessageDescriptor } from '@lingui/core';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { InfoIcon } from 'lucide-react';
import { DateTime } from 'luxon';
import { z } from 'zod';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
import type { Team } from '@documenso/prisma/client';
import { DocumentSource, DocumentStatus as DocumentStatusEnum } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { SelectItem } from '@documenso/ui/primitives/select';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
import { DocumentSearch } from '~/components/(dashboard)/document-search/document-search';
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
import { DocumentStatus } from '~/components/formatter/document-status';
import { SearchParamSelector } from '~/components/forms/search-param-selector';
import { DataTableActionButton } from '../../documents/data-table-action-button';
import { DataTableActionDropdown } from '../../documents/data-table-action-dropdown';
import { DataTableTitle } from '../../documents/data-table-title';
const DOCUMENT_SOURCE_LABELS: { [key in DocumentSource]: MessageDescriptor } = {
DOCUMENT: msg`Document`,
TEMPLATE: msg`Template`,
TEMPLATE_DIRECT_LINK: msg`Direct link`,
};
const ZTemplateSearchParamsSchema = ZBaseTableSearchParamsSchema.extend({
source: z
.nativeEnum(DocumentSource)
.optional()
.catch(() => undefined),
status: z
.nativeEnum(DocumentStatusEnum)
.optional()
.catch(() => undefined),
search: z.coerce
.string()
.optional()
.catch(() => undefined),
});
type TemplatePageViewDocumentsTableProps = {
templateId: number;
team?: Team;
};
export const TemplatePageViewDocumentsTable = ({
templateId,
team,
}: TemplatePageViewDocumentsTableProps) => {
const { _, i18n } = useLingui();
const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const parsedSearchParams = ZTemplateSearchParamsSchema.parse(
Object.fromEntries(searchParams ?? []),
);
const { data, isLoading, isInitialLoading, isLoadingError } =
trpc.document.findDocuments.useQuery(
{
templateId,
teamId: team?.id,
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
search: parsedSearchParams.search,
source: parsedSearchParams.source,
status: parsedSearchParams.status,
},
{
keepPreviousData: true,
},
);
const onPaginationChange = (page: number, perPage: number) => {
updateSearchParams({
page,
perPage,
});
};
const results = data ?? {
data: [],
perPage: 10,
currentPage: 1,
totalPages: 1,
};
const columns = useMemo(() => {
return [
{
header: _(msg`Created`),
accessorKey: 'createdAt',
cell: ({ row }) =>
i18n.date(row.original.createdAt, { ...DateTime.DATETIME_SHORT, hourCycle: 'h12' }),
},
{
header: _(msg`Title`),
cell: ({ row }) => <DataTableTitle row={row.original} teamUrl={team?.url} />,
},
{
header: _(msg`Recipient`),
accessorKey: 'recipient',
cell: ({ row }) => (
<StackAvatarsWithTooltip
recipients={row.original.Recipient}
documentStatus={row.original.status}
/>
),
},
{
header: _(msg`Status`),
accessorKey: 'status',
cell: ({ row }) => <DocumentStatus status={row.getValue('status')} />,
size: 140,
},
{
header: () => (
<div className="flex flex-row items-center">
<Trans>Source</Trans>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-foreground max-w-md space-y-2 !p-0">
<ul className="text-muted-foreground space-y-0.5 divide-y [&>li]:p-4">
<li>
<h2 className="mb-2 flex flex-row items-center font-semibold">
<Trans>Template</Trans>
</h2>
<p>
<Trans>
This document was created by you or a team member using the template above.
</Trans>
</p>
</li>
<li>
<h2 className="mb-2 flex flex-row items-center font-semibold">
<Trans>Direct Link</Trans>
</h2>
<p>
<Trans>This document was created using a direct link.</Trans>
</p>
</li>
</ul>
</TooltipContent>
</Tooltip>
</div>
),
accessorKey: 'type',
cell: ({ row }) => (
<div className="flex flex-row items-center">
{_(DOCUMENT_SOURCE_LABELS[row.original.source])}
</div>
),
},
{
id: 'actions',
header: _(msg`Actions`),
cell: ({ row }) => (
<div className="flex items-center space-x-2">
<DataTableActionButton team={team} row={row.original} />
<DataTableActionDropdown team={team} row={row.original} />
</div>
),
},
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
}, []);
return (
<div>
<div className="mb-4 flex flex-row space-x-4">
<DocumentSearch />
<SearchParamSelector
paramKey="status"
isValueValid={(value) =>
[...DocumentStatusEnum.COMPLETED].includes(value as unknown as string)
}
>
<SelectItem value="all">
<Trans>Any Status</Trans>
</SelectItem>
<SelectItem value={DocumentStatusEnum.COMPLETED}>
<Trans>Completed</Trans>
</SelectItem>
<SelectItem value={DocumentStatusEnum.PENDING}>
<Trans>Pending</Trans>
</SelectItem>
<SelectItem value={DocumentStatusEnum.DRAFT}>
<Trans>Draft</Trans>
</SelectItem>
</SearchParamSelector>
<SearchParamSelector
paramKey="source"
isValueValid={(value) =>
[...DocumentSource.TEMPLATE].includes(value as unknown as string)
}
>
<SelectItem value="all">
<Trans>Any Source</Trans>
</SelectItem>
<SelectItem value={DocumentSource.TEMPLATE}>
<Trans>Template</Trans>
</SelectItem>
<SelectItem value={DocumentSource.TEMPLATE_DIRECT_LINK}>
<Trans>Direct Link</Trans>
</SelectItem>
</SearchParamSelector>
<PeriodSelector />
</div>
<DataTable
columns={columns}
data={results.data}
perPage={results.perPage}
currentPage={results.currentPage}
totalPages={results.totalPages}
onPaginationChange={onPaginationChange}
error={{
enable: isLoadingError,
}}
skeleton={{
enable: isLoading && isInitialLoading,
rows: 3,
component: (
<>
<TableCell>
<Skeleton className="h-4 w-12 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-24 rounded-full" />
</TableCell>
<TableCell className="py-4 pr-4">
<Skeleton className="h-12 w-12 flex-shrink-0 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-12 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-12 rounded-full" />
</TableCell>
<TableCell>
<div className="flex flex-row justify-end space-x-2">
<Skeleton className="h-10 w-20 rounded" />
</div>
</TableCell>
</>
),
}}
>
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
</div>
);
};

View File

@ -0,0 +1,66 @@
'use client';
import { useMemo } from 'react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
import type { Template, User } from '@documenso/prisma/client';
export type TemplatePageViewInformationProps = {
userId: number;
template: Template & {
User: Pick<User, 'id' | 'name' | 'email'>;
};
};
export const TemplatePageViewInformation = ({
template,
userId,
}: TemplatePageViewInformationProps) => {
const isMounted = useIsMounted();
const { _, i18n } = useLingui();
const templateInformation = useMemo(() => {
return [
{
description: msg`Uploaded by`,
value: userId === template.userId ? _(msg`You`) : template.User.name ?? template.User.email,
},
{
description: msg`Created`,
value: i18n.date(template.createdAt, { dateStyle: 'medium' }),
},
{
description: msg`Last modified`,
value: DateTime.fromJSDate(template.updatedAt)
.setLocale(i18n.locales?.[0] || i18n.locale)
.toRelative(),
},
];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMounted, template, userId]);
return (
<section className="dark:bg-background text-foreground border-border bg-widget flex flex-col rounded-xl border">
<h1 className="px-4 py-3 font-medium">
<Trans>Information</Trans>
</h1>
<ul className="divide-y border-t">
{templateInformation.map((item, i) => (
<li
key={i}
className="flex items-center justify-between px-4 py-2.5 text-sm last:border-b"
>
<span className="text-muted-foreground">{_(item.description)}</span>
<span>{item.value}</span>
</li>
))}
</ul>
</section>
);
};

View File

@ -0,0 +1,163 @@
'use client';
import Link from 'next/link';
import { Trans } from '@lingui/macro';
import { Loader } from 'lucide-react';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
import { DocumentSource } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
export type TemplatePageViewRecentActivityProps = {
templateId: number;
teamId?: number;
documentRootPath: string;
};
export const TemplatePageViewRecentActivity = ({
templateId,
teamId,
documentRootPath,
}: TemplatePageViewRecentActivityProps) => {
const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocuments.useQuery({
templateId,
teamId,
orderBy: {
column: 'createdAt',
direction: 'asc',
},
perPage: 5,
});
const results = data ?? {
data: [],
perPage: 10,
currentPage: 1,
totalPages: 1,
};
return (
<section className="dark:bg-background border-border bg-widget flex flex-col rounded-xl border">
<div className="flex flex-row items-center justify-between border-b px-4 py-3">
<h1 className="text-foreground font-medium">
<Trans>Recent documents</Trans>
</h1>
{/* Can add dropdown menu here for additional options. */}
</div>
{isLoading && (
<div className="flex h-full items-center justify-center py-16">
<Loader className="text-muted-foreground h-6 w-6 animate-spin" />
</div>
)}
{isLoadingError && (
<div className="flex h-full flex-col items-center justify-center py-16">
<p className="text-foreground/80 text-sm">
<Trans>Unable to load documents</Trans>
</p>
<button
onClick={async () => refetch()}
className="text-foreground/70 hover:text-muted-foreground mt-2 text-sm"
>
<Trans>Click here to retry</Trans>
</button>
</div>
)}
{data && (
<>
<ul role="list" className="space-y-6 p-4">
{data.data.length > 0 && results.totalPages > 1 && (
<li className="relative flex gap-x-4">
<div className="absolute -bottom-6 left-0 top-0 flex w-6 justify-center">
<div className="bg-border w-px" />
</div>
<div className="bg-widget relative flex h-6 w-6 flex-none items-center justify-center">
<div className="bg-widget h-1.5 w-1.5 rounded-full ring-1 ring-gray-300 dark:ring-neutral-600" />
</div>
<button
onClick={() => {
window.scrollTo({
top: document.getElementById('documents')?.offsetTop,
behavior: 'smooth',
});
}}
className="text-foreground/70 hover:text-muted-foreground flex items-center text-xs"
>
<Trans>View more</Trans>
</button>
</li>
)}
{results.data.length === 0 && (
<div className="flex items-center justify-center py-4">
<p className="text-muted-foreground/70 text-sm">
<Trans>No recent documents</Trans>
</p>
</div>
)}
{results.data.map((document, documentIndex) => (
<li key={document.id} className="relative flex gap-x-4">
<div
className={cn(
documentIndex === results.data.length - 1 ? 'h-6' : '-bottom-6',
'absolute left-0 top-0 flex w-6 justify-center',
)}
>
<div className="bg-border w-px" />
</div>
<div className="bg-widget text-foreground/40 relative flex h-6 w-6 flex-none items-center justify-center">
<div className="bg-widget h-1.5 w-1.5 rounded-full ring-1 ring-gray-300 dark:ring-neutral-600" />
</div>
<Link
href={`${documentRootPath}/${document.id}`}
className="text-muted-foreground dark:text-muted-foreground/70 flex-auto truncate py-0.5 text-xs leading-5"
>
{match(document.source)
.with(DocumentSource.DOCUMENT, DocumentSource.TEMPLATE, () => (
<Trans>
Document created by <span className="font-bold">{document.User.name}</span>
</Trans>
))
.with(DocumentSource.TEMPLATE_DIRECT_LINK, () => (
<Trans>
Document created using a <span className="font-bold">direct link</span>
</Trans>
))
.exhaustive()}
</Link>
<time className="text-muted-foreground dark:text-muted-foreground/70 flex-none py-0.5 text-xs leading-5">
{DateTime.fromJSDate(document.createdAt).toRelative({ style: 'short' })}
</time>
</li>
))}
</ul>
<Button
className="mx-4 mb-4"
onClick={() => {
window.scrollTo({
top: document.getElementById('documents')?.offsetTop,
behavior: 'smooth',
});
}}
>
<Trans>View all related documents</Trans>
</Button>
</>
)}
</section>
);
};

View File

@ -0,0 +1,69 @@
import Link from 'next/link';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { PenIcon, PlusIcon } from 'lucide-react';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import type { Recipient, Template } from '@documenso/prisma/client';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
export type TemplatePageViewRecipientsProps = {
template: Template & {
Recipient: Recipient[];
};
templateRootPath: string;
};
export const TemplatePageViewRecipients = ({
template,
templateRootPath,
}: TemplatePageViewRecipientsProps) => {
const { _ } = useLingui();
const recipients = template.Recipient;
return (
<section className="dark:bg-background border-border bg-widget flex flex-col rounded-xl border">
<div className="flex flex-row items-center justify-between px-4 py-3">
<h1 className="text-foreground font-medium">
<Trans>Recipients</Trans>
</h1>
<Link
href={`${templateRootPath}/${template.id}/edit?step=signers`}
title={_(msg`Modify recipients`)}
className="flex flex-row items-center justify-between"
>
{recipients.length === 0 ? (
<PlusIcon className="ml-2 h-4 w-4" />
) : (
<PenIcon className="ml-2 h-3 w-3" />
)}
</Link>
</div>
<ul className="text-muted-foreground divide-y border-t">
{recipients.length === 0 && (
<li className="flex flex-col items-center justify-center py-6 text-sm">
<Trans>No recipients</Trans>
</li>
)}
{recipients.map((recipient) => (
<li key={recipient.id} className="flex items-center justify-between px-4 py-2.5 text-sm">
<AvatarWithText
avatarFallback={recipient.email.slice(0, 1).toUpperCase()}
primaryText={<p className="text-muted-foreground text-sm">{recipient.email}</p>}
secondaryText={
<p className="text-muted-foreground/70 text-xs">
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
</p>
}
/>
</li>
))}
</ul>
</section>
);
};

View File

@ -1,22 +1,28 @@
import React from 'react';
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { Trans } from '@lingui/macro';
import { ChevronLeft } from 'lucide-react';
import { ChevronLeft, LucideEdit } from 'lucide-react';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getTemplateWithDetailsById } from '@documenso/lib/server-only/template/get-template-with-details-by-id';
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
import type { Team } from '@documenso/prisma/client';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
import { DocumentSigningOrder, SigningStatus, type Team } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { DocumentReadOnlyFields } from '~/components/document/document-read-only-fields';
import { TemplateType } from '~/components/formatter/template-type';
import { DataTableActionDropdown } from '../data-table-action-dropdown';
import { TemplateDirectLinkBadge } from '../template-direct-link-badge';
import { EditTemplateForm } from './edit-template';
import { UseTemplateDialog } from '../use-template-dialog';
import { TemplateDirectLinkDialogWrapper } from './template-direct-link-dialog-wrapper';
import { TemplatePageViewDocumentsTable } from './template-page-view-documents-table';
import { TemplatePageViewInformation } from './template-page-view-information';
import { TemplatePageViewRecentActivity } from './template-page-view-recent-activity';
import { TemplatePageViewRecipients } from './template-page-view-recipients';
export type TemplatePageViewProps = {
params: {
@ -30,6 +36,7 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
const templateId = Number(id);
const templateRootPath = formatTemplatesPath(team?.url);
const documentRootPath = formatDocumentsPath(team?.url);
if (!templateId || Number.isNaN(templateId)) {
redirect(templateRootPath);
@ -37,29 +44,51 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
const { user } = await getRequiredServerComponentSession();
const template = await getTemplateWithDetailsById({
const template = await getTemplateById({
id: templateId,
userId: user.id,
teamId: team?.id,
}).catch(() => null);
if (!template || !template.templateDocumentData) {
if (!template || !template.templateDocumentData || (template?.teamId && !team?.url)) {
redirect(templateRootPath);
}
const isTemplateEnterprise = await isUserEnterprise({
userId: user.id,
teamId: team?.id,
const { templateDocumentData, Field, Recipient: recipients, templateMeta } = template;
// Remap to fit the DocumentReadOnlyFields component.
const readOnlyFields = Field.map((field) => {
const recipient = recipients.find((recipient) => recipient.id === field.recipientId) || {
name: '',
email: '',
signingStatus: SigningStatus.NOT_SIGNED,
};
return {
...field,
Recipient: recipient,
Signature: null,
};
});
const mockedDocumentMeta = templateMeta
? {
typedSignatureEnabled: false,
...templateMeta,
signingOrder: templateMeta.signingOrder || DocumentSigningOrder.SEQUENTIAL,
documentId: 0,
}
: undefined;
return (
<div className="mx-auto -mt-4 max-w-screen-xl px-4 md:px-8">
<div className="flex flex-col justify-between sm:flex-row">
<div>
<Link href="/templates" className="flex items-center text-[#7AC455] hover:opacity-80">
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
<Link href={templateRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
<Trans>Templates</Trans>
</Link>
<div className="flex flex-row justify-between truncate">
<div>
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={template.title}>
{template.title}
</h1>
@ -77,17 +106,97 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
</div>
</div>
<div className="mt-2 sm:mt-0 sm:self-end">
<div className="mt-2 flex flex-row space-x-4 sm:mt-0 sm:self-end">
<TemplateDirectLinkDialogWrapper template={template} />
<Button className="w-full" asChild>
<Link href={`${templateRootPath}/${template.id}/edit`}>
<LucideEdit className="mr-1.5 h-3.5 w-3.5" />
<Trans>Edit Template</Trans>
</Link>
</Button>
</div>
</div>
<EditTemplateForm
className="mt-6"
initialTemplate={template}
templateRootPath={templateRootPath}
isEnterprise={isTemplateEnterprise}
<div className="mt-6 grid w-full grid-cols-12 gap-8">
<Card
className="relative col-span-12 rounded-xl before:rounded-xl lg:col-span-6 xl:col-span-7"
gradient
>
<CardContent className="p-2">
<LazyPDFViewer
document={template}
key={template.id}
documentData={templateDocumentData}
/>
</CardContent>
</Card>
<DocumentReadOnlyFields
fields={readOnlyFields}
showFieldStatus={false}
documentMeta={mockedDocumentMeta}
/>
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
<div className="space-y-6">
<section className="border-border bg-widget flex flex-col rounded-xl border pb-4 pt-6">
<div className="flex flex-row items-center justify-between px-4">
<h3 className="text-foreground text-2xl font-semibold">
<Trans>Template</Trans>
</h3>
<div>
<DataTableActionDropdown
row={template}
teamId={team?.id}
templateRootPath={templateRootPath}
/>
</div>
</div>
<p className="text-muted-foreground mt-2 px-4 text-sm ">
<Trans>Manage and view template</Trans>
</p>
<div className="mt-4 border-t px-4 pt-4">
<UseTemplateDialog
templateId={template.id}
templateSigningOrder={template.templateMeta?.signingOrder}
recipients={template.Recipient}
documentRootPath={documentRootPath}
trigger={
<Button className="w-full">
<Trans>Use</Trans>
</Button>
}
/>
</div>
</section>
{/* Template information section. */}
<TemplatePageViewInformation template={template} userId={user.id} />
{/* Recipients section. */}
<TemplatePageViewRecipients template={template} templateRootPath={templateRootPath} />
{/* Recent activity section. */}
<TemplatePageViewRecentActivity
documentRootPath={documentRootPath}
templateId={template.id}
teamId={team?.id}
/>
</div>
</div>
</div>
<div className="mt-16" id="documents">
<h1 className="mb-4 text-2xl font-bold">
<Trans>Documents created from template</Trans>
</h1>
<TemplatePageViewDocumentsTable team={team} templateId={template.id} />
</div>
</div>
);
};

View File

@ -8,7 +8,7 @@ import { Trans } from '@lingui/macro';
import { Copy, Edit, MoreHorizontal, MoveRight, Share2Icon, Trash2 } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { type FindTemplateRow } from '@documenso/lib/server-only/template/find-templates';
import type { Recipient, Template, TemplateDirectLink } from '@documenso/prisma/client';
import {
DropdownMenu,
DropdownMenuContent,
@ -23,7 +23,10 @@ import { MoveTemplateDialog } from './move-template-dialog';
import { TemplateDirectLinkDialog } from './template-direct-link-dialog';
export type DataTableActionDropdownProps = {
row: FindTemplateRow;
row: Template & {
directLink?: Pick<TemplateDirectLink, 'token' | 'enabled'> | null;
Recipient: Recipient[];
};
templateRootPath: string;
teamId?: number;
};
@ -57,7 +60,7 @@ export const DataTableActionDropdown = ({
<DropdownMenuLabel>Action</DropdownMenuLabel>
<DropdownMenuItem disabled={!isOwner && !isTeamTemplate} asChild>
<Link href={`${templateRootPath}/${row.id}`}>
<Link href={`${templateRootPath}/${row.id}/edit`}>
<Edit className="mr-2 h-4 w-4" />
<Trans>Edit</Trans>
</Link>

View File

@ -124,7 +124,7 @@ export const TemplatesDataTable = ({
accessorKey: 'type',
cell: ({ row }) => (
<div className="flex flex-row items-center">
<TemplateType type="PRIVATE" />
<TemplateType type={row.original.type} />
{row.original.directLink?.token && (
<TemplateDirectLinkBadge

View File

@ -73,7 +73,7 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo
setShowNewTemplateDialog(false);
router.push(`${templateRootPath}/${id}`);
router.push(`${templateRootPath}/${id}/edit`);
} catch {
toast({
title: _(msg`Something went wrong`),

View File

@ -1,3 +1,5 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
@ -92,6 +94,7 @@ export type UseTemplateDialogProps = {
templateSigningOrder?: DocumentSigningOrder | null;
recipients: Recipient[];
documentRootPath: string;
trigger?: React.ReactNode;
};
export function UseTemplateDialog({
@ -99,6 +102,7 @@ export function UseTemplateDialog({
documentRootPath,
templateId,
templateSigningOrder,
trigger,
}: UseTemplateDialogProps) {
const router = useRouter();
@ -186,10 +190,12 @@ export function UseTemplateDialog({
return (
<Dialog open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
<DialogTrigger asChild>
{trigger || (
<Button variant="outline" className="bg-background">
<Plus className="-ml-1 mr-2 h-4 w-4" />
<Trans>Use Template</Trans>
</Button>
)}
</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<DialogHeader>

View File

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

View File

@ -2,13 +2,18 @@ import React from 'react';
import { redirect } from 'next/navigation';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
import { RECIPIENT_ROLES_DESCRIPTION_ENG } from '@documenso/lib/constants/recipient-roles';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
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 { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
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 { 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) {
const { i18n } = await setupI18nSSR();
const { _ } = useLingui();
const { d } = searchParams;
if (typeof d !== 'string' || !d) {
@ -44,6 +59,10 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
return redirect('/');
}
const documentLanguage = ZSupportedLanguageCodeSchema.parse(document.documentMeta?.language);
await dynamicActivate(i18n, documentLanguage);
const { data: auditLogs } = await findDocumentAuditLogs({
documentId: documentId,
userId: document.userId,
@ -53,31 +72,35 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
return (
<div className="print-provider pointer-events-none mx-auto max-w-screen-md">
<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>
<Card>
<CardContent className="grid grid-cols-2 gap-4 p-6 text-sm print:text-xs">
<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>
</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>
</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>
<span className="font-medium">Owner</span>
<span className="font-medium">{_(msg`Owner`)}</span>
<span className="mt-1 block break-words">
{document.User.name} ({document.User.email})
@ -85,7 +108,7 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
</p>
<p>
<span className="font-medium">Created At</span>
<span className="font-medium">{_(msg`Created At`)}</span>
<span className="mt-1 block">
{DateTime.fromJSDate(document.createdAt)
@ -95,7 +118,7 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
</p>
<p>
<span className="font-medium">Last Updated</span>
<span className="font-medium">{_(msg`Last Updated`)}</span>
<span className="mt-1 block">
{DateTime.fromJSDate(document.updatedAt)
@ -105,7 +128,7 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
</p>
<p>
<span className="font-medium">Time Zone</span>
<span className="font-medium">{_(msg`Time Zone`)}</span>
<span className="mt-1 block break-words">
{document.documentMeta?.timezone ?? 'N/A'}
@ -113,13 +136,13 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
</p>
<div>
<p className="font-medium">Recipients</p>
<p className="font-medium">{_(msg`Recipients`)}</p>
<ul className="mt-1 list-inside list-disc">
{document.Recipient.map((recipient) => (
<li key={recipient.id}>
<span className="text-muted-foreground">
[{RECIPIENT_ROLES_DESCRIPTION_ENG[recipient.role].roleName}]
[{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}]
</span>{' '}
{recipient.name} ({recipient.email})
</li>

View File

@ -2,20 +2,24 @@ import React from 'react';
import { redirect } from 'next/navigation';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
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 {
RECIPIENT_ROLES_DESCRIPTION_ENG,
RECIPIENT_ROLE_SIGNING_REASONS_ENG,
RECIPIENT_ROLES_DESCRIPTION,
RECIPIENT_ROLE_SIGNING_REASONS,
} from '@documenso/lib/constants/recipient-roles';
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
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 { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { dynamicActivate } from '@documenso/lib/utils/i18n';
import { FieldType } from '@documenso/prisma/client';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import {
@ -36,11 +40,21 @@ type SigningCertificateProps = {
};
const FRIENDLY_SIGNING_REASONS = {
['__OWNER__']: `I am the owner of this document`,
...RECIPIENT_ROLE_SIGNING_REASONS_ENG,
['__OWNER__']: msg`I am the owner of this document`,
...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) {
const { i18n } = await setupI18nSSR();
const { _ } = useLingui();
const { d } = searchParams;
if (typeof d !== 'string' || !d) {
@ -63,6 +77,10 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
return redirect('/');
}
const documentLanguage = ZSupportedLanguageCodeSchema.parse(document.documentMeta?.language);
await dynamicActivate(i18n, documentLanguage);
const auditLogs = await getDocumentCertificateAuditLogs({
id: documentId,
});
@ -98,17 +116,17 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
});
let authLevel = match(extractedAuthMethods.derivedRecipientActionAuth)
.with('ACCOUNT', () => 'Account Re-Authentication')
.with('TWO_FACTOR_AUTH', () => 'Two-Factor Re-Authentication')
.with('PASSKEY', () => 'Passkey Re-Authentication')
.with('EXPLICIT_NONE', () => 'Email')
.with('ACCOUNT', () => _(msg`Account Re-Authentication`))
.with('TWO_FACTOR_AUTH', () => _(msg`Two-Factor Re-Authentication`))
.with('PASSKEY', () => _(msg`Passkey Re-Authentication`))
.with('EXPLICIT_NONE', () => _(msg`Email`))
.with(null, () => null)
.exhaustive();
if (!authLevel) {
authLevel = match(extractedAuthMethods.derivedRecipientAccessAuth)
.with('ACCOUNT', () => 'Account Authentication')
.with(null, () => 'Email')
.with('ACCOUNT', () => _(msg`Account Authentication`))
.with(null, () => _(msg`Email`))
.exhaustive();
}
@ -147,7 +165,7 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
return (
<div className="print-provider pointer-events-none mx-auto max-w-screen-md">
<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>
<Card>
@ -155,9 +173,9 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
<Table overflowHidden>
<TableHeader>
<TableRow>
<TableHead>Signer Events</TableHead>
<TableHead>Signature</TableHead>
<TableHead>Details</TableHead>
<TableHead>{_(msg`Signer Events`)}</TableHead>
<TableHead>{_(msg`Signature`)}</TableHead>
<TableHead>{_(msg`Details`)}</TableHead>
{/* <TableHead>Security</TableHead> */}
</TableRow>
</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="break-all">{recipient.email}</div>
<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 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>
</p>
</TableCell>
@ -199,21 +217,21 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
</div>
<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">
{signature.secondaryId}
</span>
</p>
<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">
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.ipAddress ?? 'Unknown'}
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.ipAddress ?? _(msg`Unknown`)}
</span>
</p>
<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">
{getDevice(logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.userAgent)}
</span>
@ -227,44 +245,46 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
<TableCell truncate={false} className="w-[min-content] align-top">
<div className="space-y-1">
<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">
{logs.EMAIL_SENT[0]
? DateTime.fromJSDate(logs.EMAIL_SENT[0].createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
: 'Unknown'}
: _(msg`Unknown`)}
</span>
</p>
<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">
{logs.DOCUMENT_OPENED[0]
? DateTime.fromJSDate(logs.DOCUMENT_OPENED[0].createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
: 'Unknown'}
: _(msg`Unknown`)}
</span>
</p>
<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">
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]
? DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
: 'Unknown'}
: _(msg`Unknown`)}
</span>
</p>
<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">
{isOwner(recipient.email)
{_(
isOwner(recipient.email)
? FRIENDLY_SIGNING_REASONS['__OWNER__']
: FRIENDLY_SIGNING_REASONS[recipient.role]}
: FRIENDLY_SIGNING_REASONS[recipient.role],
)}
</span>
</p>
</div>
@ -280,7 +300,7 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
<div className="my-8 flex-row-reverse">
<div className="flex items-end justify-end gap-x-4">
<p className="flex-shrink-0 text-sm font-medium print:text-xs">
Signing certificate provided by:
{_(msg`Signing certificate provided by`)}:
</p>
<Logo className="max-h-6 print:max-h-4" />

View File

@ -0,0 +1,24 @@
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import type { TemplateEditPageViewProps } from '~/app/(dashboard)/templates/[id]/edit/template-edit-page-view';
import { TemplateEditPageView } from '~/app/(dashboard)/templates/[id]/edit/template-edit-page-view';
export type TeamsTemplateEditPageProps = {
params: TemplateEditPageViewProps['params'] & {
teamUrl: string;
};
};
export default async function TeamsTemplateEditPage({ params }: TeamsTemplateEditPageProps) {
await setupI18nSSR();
const { teamUrl } = params;
const { user } = await getRequiredServerComponentSession();
const team = await getTeamByUrl({ userId: user.id, teamUrl });
return <TemplateEditPageView params={params} team={team} />;
}

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_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
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 { cn } from '@documenso/ui/lib/utils';
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
@ -37,7 +37,7 @@ export const DocumentHistorySheet = ({
onMenuOpenChange,
children,
}: DocumentHistorySheetProps) => {
const { i18n } = useLingui();
const { _, i18n } = useLingui();
const [isUserDetailsVisible, setIsUserDetailsVisible] = useState(false);
@ -152,7 +152,7 @@ export const DocumentHistorySheet = ({
<div>
<p className="text-foreground text-xs font-bold">
{formatDocumentAuditLogActionString(auditLog, userId)}
{formatDocumentAuditLogAction(_, auditLog, userId).description}
</p>
<p className="text-foreground/50 text-xs">
{DateTime.fromJSDate(auditLog.createdAt)

View File

@ -2,8 +2,9 @@
import { useState } from 'react';
import { Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { EyeOffIcon } from 'lucide-react';
import { Clock, EyeOffIcon } from 'lucide-react';
import { P, match } from 'ts-pattern';
import {
@ -18,8 +19,10 @@ import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import type { DocumentMeta } from '@documenso/prisma/client';
import { FieldType, SigningStatus } from '@documenso/prisma/client';
import { FieldRootContainer } from '@documenso/ui/components/field/field';
import { SignatureIcon } from '@documenso/ui/icons/signature';
import { cn } from '@documenso/ui/lib/utils';
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
import { Badge } from '@documenso/ui/primitives/badge';
import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { PopoverHover } from '@documenso/ui/primitives/popover';
@ -27,9 +30,14 @@ import { PopoverHover } from '@documenso/ui/primitives/popover';
export type DocumentReadOnlyFieldsProps = {
fields: DocumentField[];
documentMeta?: DocumentMeta;
showFieldStatus?: boolean;
};
export const DocumentReadOnlyFields = ({ documentMeta, fields }: DocumentReadOnlyFieldsProps) => {
export const DocumentReadOnlyFields = ({
documentMeta,
fields,
showFieldStatus = true,
}: DocumentReadOnlyFieldsProps) => {
const { _ } = useLingui();
const [hiddenFieldIds, setHiddenFieldIds] = useState<Record<string, boolean>>({});
@ -58,15 +66,37 @@ export const DocumentReadOnlyFields = ({ documentMeta, fields }: DocumentReadOnl
</Avatar>
}
contentProps={{
className: 'relative flex w-fit flex-col p-2.5 text-sm',
className: 'relative flex w-fit flex-col p-4 text-sm',
}}
>
<p className="font-semibold">
{field.Recipient.signingStatus === SigningStatus.SIGNED ? 'Signed' : 'Pending'}{' '}
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type]).toLowerCase()} field
{showFieldStatus && (
<Badge
className="mx-auto mb-1 py-0.5"
variant={
field.Recipient.signingStatus === SigningStatus.SIGNED
? 'default'
: 'secondary'
}
>
{field.Recipient.signingStatus === SigningStatus.SIGNED ? (
<>
<SignatureIcon className="mr-1 h-3 w-3" />
<Trans>Signed</Trans>
</>
) : (
<>
<Clock className="mr-1 h-3 w-3" />
<Trans>Pending</Trans>
</>
)}
</Badge>
)}
<p className="text-center font-semibold">
<span>{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])} field</span>
</p>
<p className="text-muted-foreground text-xs">
<p className="text-muted-foreground mt-1 text-center text-xs">
{field.Recipient.name
? `${field.Recipient.name} (${field.Recipient.email})`
: field.Recipient.email}{' '}

View File

@ -0,0 +1,151 @@
'use client';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { formatSigningLink } from '@documenso/lib/utils/recipients';
import type { Recipient } from '@documenso/prisma/client';
import { RecipientRole } from '@documenso/prisma/client';
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type DocumentRecipientLinkCopyDialogProps = {
trigger?: React.ReactNode;
recipients: Recipient[];
};
export const DocumentRecipientLinkCopyDialog = ({
trigger,
recipients,
}: DocumentRecipientLinkCopyDialogProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const [, copy] = useCopyToClipboard();
const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const [open, setOpen] = useState(false);
const actionSearchParam = searchParams?.get('action');
const onBulkCopy = async () => {
const generatedString = recipients
.filter((recipient) => recipient.role !== RecipientRole.CC)
.map((recipient) => `${recipient.email}\n${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`)
.join('\n\n');
await copy(generatedString).then(() => {
toast({
title: _(msg`Copied to clipboard`),
description: _(msg`All signing links have been copied to your clipboard.`),
});
});
};
useEffect(() => {
if (actionSearchParam === 'view-signing-links') {
setOpen(true);
updateSearchParams({ action: null });
}
}, [actionSearchParam, open, setOpen, updateSearchParams]);
return (
<Dialog open={open} onOpenChange={(value) => setOpen(value)}>
<DialogTrigger asChild onClick={(e) => e.stopPropagation()}>
{trigger}
</DialogTrigger>
<DialogContent position="center">
<DialogHeader>
<DialogTitle className="pb-0.5">
<Trans>Copy Signing Links</Trans>
</DialogTitle>
<DialogDescription>
<Trans>
You can copy and share these links to recipients so they can action the document.
</Trans>
</DialogDescription>
</DialogHeader>
<ul className="text-muted-foreground divide-y rounded-lg border">
{recipients.length === 0 && (
<li className="flex flex-col items-center justify-center py-6 text-sm">
<Trans>No recipients</Trans>
</li>
)}
{recipients.map((recipient) => (
<li key={recipient.id} className="flex items-center justify-between px-4 py-3 text-sm">
<AvatarWithText
avatarFallback={recipient.email.slice(0, 1).toUpperCase()}
primaryText={<p className="text-muted-foreground text-sm">{recipient.email}</p>}
secondaryText={
<p className="text-muted-foreground/70 text-xs">
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
</p>
}
/>
{recipient.role !== RecipientRole.CC && (
<CopyTextButton
value={formatSigningLink(recipient.token)}
onCopySuccess={() => {
toast({
title: _(msg`Copied to clipboard`),
description: _(msg`The signing link has been copied to your clipboard.`),
});
}}
badgeContentUncopied={
<p className="ml-1 text-xs">
<Trans>Copy</Trans>
</p>
}
badgeContentCopied={
<p className="ml-1 text-xs">
<Trans>Copied</Trans>
</p>
}
/>
)}
</li>
))}
</ul>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="secondary">
<Trans>Close</Trans>
</Button>
</DialogClose>
<Button type="button" onClick={onBulkCopy}>
<Trans>Bulk Copy</Trans>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@ -0,0 +1,50 @@
import React, { useMemo } from 'react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { Select, SelectContent, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
export type SearchParamSelector = {
paramKey: string;
isValueValid: (value: unknown) => boolean;
children: React.ReactNode;
};
export const SearchParamSelector = ({ children, paramKey, isValueValid }: SearchParamSelector) => {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();
const value = useMemo(() => {
const p = searchParams?.get(paramKey) ?? 'all';
return isValueValid(p) ? p : 'all';
}, [searchParams]);
const onValueChange = (newValue: string) => {
if (!pathname) {
return;
}
const params = new URLSearchParams(searchParams?.toString());
params.set(paramKey, newValue);
if (newValue === '' || newValue === 'all') {
params.delete(paramKey);
}
router.push(`${pathname}?${params.toString()}`, { scroll: false });
};
return (
<Select defaultValue={value} onValueChange={onValueChange}>
<SelectTrigger className="text-muted-foreground max-w-[200px]">
<SelectValue />
</SelectTrigger>
<SelectContent position="popper">{children}</SelectContent>
</Select>
);
};

View File

@ -50,6 +50,7 @@ services:
- NEXT_PRIVATE_SMTP_SECURE=${NEXT_PRIVATE_SMTP_SECURE}
- NEXT_PRIVATE_SMTP_FROM_NAME=${NEXT_PRIVATE_SMTP_FROM_NAME:?err}
- NEXT_PRIVATE_SMTP_FROM_ADDRESS=${NEXT_PRIVATE_SMTP_FROM_ADDRESS:?err}
- NEXT_PRIVATE_SMTP_SERVICE=${NEXT_PRIVATE_SMTP_SERVICE}
- NEXT_PRIVATE_RESEND_API_KEY=${NEXT_PRIVATE_RESEND_API_KEY}
- NEXT_PRIVATE_MAILCHANNELS_API_KEY=${NEXT_PRIVATE_MAILCHANNELS_API_KEY}
- NEXT_PRIVATE_MAILCHANNELS_ENDPOINT=${NEXT_PRIVATE_MAILCHANNELS_ENDPOINT}
@ -60,6 +61,7 @@ services:
- NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY}
- NEXT_PUBLIC_DISABLE_SIGNUP=${NEXT_PUBLIC_DISABLE_SIGNUP}
- NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=${NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH:-/opt/documenso/cert.p12}
- NEXT_PRIVATE_SIGNING_PASSPHRASE=${NEXT_PRIVATE_SIGNING_PASSPHRASE}
ports:
- ${PORT:-3000}:${PORT:-3000}
volumes:

8
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "@documenso/root",
"version": "1.7.2-rc.3",
"version": "1.7.2-rc.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@documenso/root",
"version": "1.7.2-rc.3",
"version": "1.7.2-rc.4",
"workspaces": [
"apps/*",
"packages/*"
@ -80,7 +80,7 @@
},
"apps/marketing": {
"name": "@documenso/marketing",
"version": "1.7.2-rc.3",
"version": "1.7.2-rc.4",
"license": "AGPL-3.0",
"dependencies": {
"@documenso/assets": "*",
@ -441,7 +441,7 @@
},
"apps/web": {
"name": "@documenso/web",
"version": "1.7.2-rc.3",
"version": "1.7.2-rc.4",
"license": "AGPL-3.0",
"dependencies": {
"@documenso/api": "*",

View File

@ -1,6 +1,6 @@
{
"private": true,
"version": "1.7.2-rc.3",
"version": "1.7.2-rc.4",
"scripts": {
"build": "turbo run build",
"build:web": "turbo run build --filter=@documenso/web",

View File

@ -32,7 +32,7 @@ test.describe('[EE_ONLY]', () => {
await apiSignin({
page,
email: user.email,
redirectPath: `/templates/${template.id}`,
redirectPath: `/templates/${template.id}/edit`,
});
// Set EE action auth.
@ -74,7 +74,7 @@ test.describe('[EE_ONLY]', () => {
await apiSignin({
page,
email: teamMemberUser.email,
redirectPath: `/t/${team.url}/templates/${template.id}`,
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
});
// Set EE action auth.
@ -110,7 +110,7 @@ test.describe('[EE_ONLY]', () => {
await apiSignin({
page,
email: teamMemberUser.email,
redirectPath: `/templates/${template.id}`,
redirectPath: `/templates/${template.id}/edit`,
});
// Global action auth should not be visible.
@ -132,7 +132,7 @@ test('[TEMPLATE_FLOW]: add settings', async ({ page }) => {
await apiSignin({
page,
email: user.email,
redirectPath: `/templates/${template.id}`,
redirectPath: `/templates/${template.id}/edit`,
});
// Set title.

View File

@ -31,7 +31,7 @@ test.describe('[EE_ONLY]', () => {
await apiSignin({
page,
email: user.email,
redirectPath: `/templates/${template.id}`,
redirectPath: `/templates/${template.id}/edit`,
});
// Save the settings by going to the next step.
@ -81,7 +81,7 @@ test('[TEMPLATE_FLOW]: add placeholder', async ({ page }) => {
await apiSignin({
page,
email: user.email,
redirectPath: `/templates/${template.id}`,
redirectPath: `/templates/${template.id}/edit`,
});
// Save the settings by going to the next step.

View File

@ -37,7 +37,7 @@ test('[TEMPLATE]: should create a document from a template', async ({ page }) =>
await apiSignin({
page,
email: user.email,
redirectPath: `/templates/${template.id}`,
redirectPath: `/templates/${template.id}/edit`,
});
// Set template title.
@ -172,7 +172,7 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
await apiSignin({
page,
email: owner.email,
redirectPath: `/t/${team.url}/templates/${template.id}`,
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
});
// Set template title.

View File

@ -1,10 +1,56 @@
import type { Transporter } from 'nodemailer';
import { createTransport } from 'nodemailer';
import { ResendTransport } from '@documenso/nodemailer-resend';
import { MailChannelsTransport } from './transports/mailchannels';
const getTransport = () => {
/**
* Creates a Nodemailer transport object for sending emails.
*
* This function uses various environment variables to configure the appropriate
* email transport mechanism. It supports multiple types of email transports,
* including MailChannels, Resend, and different SMTP configurations.
*
* @returns {Transporter} A configured Nodemailer transporter instance.
*
* Supported Transports:
* - **mailchannels**: Uses MailChannelsTransport, requiring:
* - `NEXT_PRIVATE_MAILCHANNELS_API_KEY`: API key for MailChannels
* - `NEXT_PRIVATE_MAILCHANNELS_ENDPOINT`: Endpoint for MailChannels (optional)
* - **resend**: Uses ResendTransport, requiring:
* - `NEXT_PRIVATE_RESEND_API_KEY`: API key for Resend
* - **smtp-api**: Uses a custom SMTP API configuration, requiring:
* - `NEXT_PRIVATE_SMTP_HOST`: The SMTP server host
* - `NEXT_PRIVATE_SMTP_APIKEY`: The API key for SMTP authentication
* - `NEXT_PRIVATE_SMTP_APIKEY_USER`: The username for SMTP authentication (default: 'apikey')
* - **smtp-auth** (default): Uses a standard SMTP configuration, requiring:
* - `NEXT_PRIVATE_SMTP_HOST`: The SMTP server host (default: 'localhost:2500')
* - `NEXT_PRIVATE_SMTP_PORT`: The port to connect to (default: 587)
* - `NEXT_PRIVATE_SMTP_SECURE`: Whether to use SSL/TLS (default: false)
* - `NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS`: Whether to ignore TLS (default: false)
* - `NEXT_PRIVATE_SMTP_USERNAME`: The username for SMTP authentication
* - `NEXT_PRIVATE_SMTP_PASSWORD`: The password for SMTP authentication
* - `NEXT_PRIVATE_SMTP_SERVICE`: The SMTP service provider (e.g., "gmail"). This option is used
* when integrating with well-known services (like Gmail), enabling simplified configuration.
*
* Example Usage:
* ```env
* NEXT_PRIVATE_SMTP_TRANSPORT='smtp-auth';
* NEXT_PRIVATE_SMTP_HOST='smtp.example.com';
* NEXT_PRIVATE_SMTP_PORT=587;
* NEXT_PRIVATE_SMTP_SERVICE='gmail';
* NEXT_PRIVATE_SMTP_SECURE='true';
* NEXT_PRIVATE_SMTP_USERNAME='your-email@gmail.com';
* NEXT_PRIVATE_SMTP_PASSWORD='your-password';
* ```
*
* Notes:
* - Ensure that the required environment variables for each transport type are set.
* - If `NEXT_PRIVATE_SMTP_TRANSPORT` is not specified, the default is `smtp-auth`.
* - `NEXT_PRIVATE_SMTP_SERVICE` is optional and used specifically for well-known services like Gmail.
*/
const getTransport = (): Transporter => {
const transport = process.env.NEXT_PRIVATE_SMTP_TRANSPORT ?? 'smtp-auth';
if (transport === 'mailchannels') {
@ -53,6 +99,9 @@ const getTransport = () => {
pass: process.env.NEXT_PRIVATE_SMTP_PASSWORD ?? '',
}
: undefined,
...(process.env.NEXT_PRIVATE_SMTP_SERVICE
? { service: process.env.NEXT_PRIVATE_SMTP_SERVICE }
: {}),
});
};

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.VIEWER]: msg`I am a viewer of this document`,
} 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

@ -115,7 +115,9 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
if (isTeamDocument && team) {
emailSubject = i18n._(msg`${team.name} invited you to ${recipientActionVerb} a document`);
emailMessage = i18n._(
emailMessage =
customEmail?.message ||
i18n._(
msg`${user.name} on behalf of ${team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`,
);
}

View File

@ -2,12 +2,15 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { mailer } from '@documenso/email/mailer';
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
import { prisma } from '@documenso/prisma';
import type { Document, DocumentMeta, Recipient, User } from '@documenso/prisma/client';
import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
import { getI18nInstance } from '../../client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
@ -192,10 +195,12 @@ const handleDocumentOwnerDelete = async ({
});
const [html, text] = await Promise.all([
renderEmailWithI18N(template),
renderEmailWithI18N(template, { plainText: true }),
renderEmailWithI18N(template, { lang: document.documentMeta?.language }),
renderEmailWithI18N(template, { lang: document.documentMeta?.language, plainText: true }),
]);
const i18n = await getI18nInstance(document.documentMeta?.language);
await mailer.sendMail({
to: {
address: recipient.email,
@ -205,7 +210,7 @@ const handleDocumentOwnerDelete = async ({
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: 'Document Cancelled',
subject: i18n._(msg`Document Cancelled`),
html,
text,
});

View File

@ -3,7 +3,14 @@ import { P, match } from 'ts-pattern';
import { prisma } from '@documenso/prisma';
import { RecipientRole, SigningStatus, TeamMemberRole } from '@documenso/prisma/client';
import type { Document, Prisma, Team, TeamEmail, User } from '@documenso/prisma/client';
import type {
Document,
DocumentSource,
Prisma,
Team,
TeamEmail,
User,
} from '@documenso/prisma/client';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { DocumentVisibility } from '../../types/document-visibility';
@ -16,6 +23,8 @@ export type FindDocumentsOptions = {
userId: number;
teamId?: number;
term?: string;
templateId?: number;
source?: DocumentSource;
status?: ExtendedDocumentStatus;
page?: number;
perPage?: number;
@ -32,6 +41,8 @@ export const findDocuments = async ({
userId,
teamId,
term,
templateId,
source,
status = ExtendedDocumentStatus.ALL,
page = 1,
perPage = 10,
@ -40,8 +51,7 @@ export const findDocuments = async ({
senderIds,
search,
}: FindDocumentsOptions) => {
const { user, team } = await prisma.$transaction(async (tx) => {
const user = await tx.user.findFirstOrThrow({
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
@ -50,7 +60,7 @@ export const findDocuments = async ({
let team = null;
if (teamId !== undefined) {
team = await tx.team.findFirstOrThrow({
team = await prisma.team.findFirstOrThrow({
where: {
id: teamId,
members: {
@ -73,12 +83,6 @@ export const findDocuments = async ({
});
}
return {
user,
team,
};
});
const orderByColumn = orderBy?.column ?? 'createdAt';
const orderByDirection = orderBy?.direction ?? 'desc';
const teamMemberRole = team?.members[0].role ?? null;
@ -197,8 +201,27 @@ export const findDocuments = async ({
};
}
const whereAndClause: Prisma.DocumentWhereInput['AND'] = [
{ ...termFilters },
{ ...filters },
{ ...deletedFilter },
{ ...searchFilter },
];
if (templateId) {
whereAndClause.push({
templateId,
});
}
if (source) {
whereAndClause.push({
source,
});
}
const whereClause: Prisma.DocumentWhereInput = {
AND: [{ ...termFilters }, { ...filters }, { ...deletedFilter }, { ...searchFilter }],
AND: whereAndClause,
};
if (period) {

View File

@ -106,17 +106,25 @@ export const resendDocument = async ({
._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb)
.toLowerCase();
let emailMessage = msg`${customEmail?.message || ''}`;
let emailSubject = msg`Reminder: Please ${recipientActionVerb} this document`;
let emailMessage = customEmail?.message || '';
let emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} this document`);
if (selfSigner) {
emailMessage = msg`You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`;
emailSubject = msg`Reminder: Please ${recipientActionVerb} your document`;
emailMessage = i18n._(
msg`You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`,
);
emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} your document`);
}
if (isTeamDocument && document.team) {
emailSubject = msg`Reminder: ${document.team.name} invited you to ${recipientActionVerb} a document`;
emailMessage = msg`${user.name} on behalf of ${document.team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`;
emailSubject = i18n._(
msg`Reminder: ${document.team.name} invited you to ${recipientActionVerb} a document`,
);
emailMessage =
customEmail?.message ||
i18n._(
msg`${user.name} on behalf of ${document.team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`,
);
}
const customEmailTemplate = {
@ -134,7 +142,7 @@ export const resendDocument = async ({
inviterEmail: isTeamDocument ? document.team?.teamEmail?.email || user.email : user.email,
assetBaseUrl,
signDocumentLink,
customBody: renderCustomEmailTemplate(i18n._(emailMessage), customEmailTemplate),
customBody: renderCustomEmailTemplate(emailMessage, customEmailTemplate),
role: recipient.role,
selfSigner,
isTeamInvite: isTeamDocument,
@ -165,7 +173,7 @@ export const resendDocument = async ({
i18n._(msg`Reminder: ${customEmail.subject}`),
customEmailTemplate,
)
: i18n._(emailSubject),
: emailSubject,
html,
text,
});

View File

@ -42,6 +42,13 @@ export const getTemplateById = async ({ id, userId, teamId }: GetTemplateByIdOpt
templateMeta: true,
Recipient: true,
Field: true,
User: {
select: {
id: true,
name: true,
email: true,
},
},
},
});

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: de\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-11-05 02:04\n"
"PO-Revision-Date: 2024-11-05 09:34\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@ -96,6 +96,90 @@ msgstr "{memberEmail} ist dem folgenden Team beigetreten"
msgid "{memberEmail} left the following team"
msgstr "{memberEmail} hat das folgende Team verlassen"
#: packages/lib/utils/document-audit-logs.ts:263
msgid "{prefix} added a field"
msgstr "{prefix} hat ein Feld hinzugefügt"
#: packages/lib/utils/document-audit-logs.ts:275
msgid "{prefix} added a recipient"
msgstr "{prefix} hat einen Empfänger hinzugefügt"
#: packages/lib/utils/document-audit-logs.ts:287
msgid "{prefix} created the document"
msgstr "{prefix} hat das Dokument erstellt"
#: packages/lib/utils/document-audit-logs.ts:291
msgid "{prefix} deleted the document"
msgstr "{prefix} hat das Dokument gelöscht"
#: packages/lib/utils/document-audit-logs.ts:335
msgid "{prefix} moved the document to team"
msgstr "{prefix} hat das Dokument ins Team verschoben"
#: packages/lib/utils/document-audit-logs.ts:319
msgid "{prefix} opened the document"
msgstr "{prefix} hat das Dokument geöffnet"
#: packages/lib/utils/document-audit-logs.ts:267
msgid "{prefix} removed a field"
msgstr "{prefix} hat ein Feld entfernt"
#: packages/lib/utils/document-audit-logs.ts:279
msgid "{prefix} removed a recipient"
msgstr "{prefix} hat einen Empfänger entfernt"
#: packages/lib/utils/document-audit-logs.ts:355
msgid "{prefix} resent an email to {0}"
msgstr "{prefix} hat eine E-Mail an {0} erneut gesendet"
#: packages/lib/utils/document-audit-logs.ts:356
msgid "{prefix} sent an email to {0}"
msgstr "{prefix} hat eine E-Mail an {0} gesendet"
#: packages/lib/utils/document-audit-logs.ts:331
msgid "{prefix} sent the document"
msgstr "{prefix} hat das Dokument gesendet"
#: packages/lib/utils/document-audit-logs.ts:295
msgid "{prefix} signed a field"
msgstr "{prefix} hat ein Feld unterschrieben"
#: packages/lib/utils/document-audit-logs.ts:299
msgid "{prefix} unsigned a field"
msgstr "{prefix} hat ein Feld ungültig gemacht"
#: packages/lib/utils/document-audit-logs.ts:271
msgid "{prefix} updated a field"
msgstr "{prefix} hat ein Feld aktualisiert"
#: packages/lib/utils/document-audit-logs.ts:283
msgid "{prefix} updated a recipient"
msgstr "{prefix} hat einen Empfänger aktualisiert"
#: packages/lib/utils/document-audit-logs.ts:315
msgid "{prefix} updated the document"
msgstr "{prefix} hat das Dokument aktualisiert"
#: packages/lib/utils/document-audit-logs.ts:307
msgid "{prefix} updated the document access auth requirements"
msgstr "{prefix} hat die Anforderungen an die Dokumentenzugriffsautorisierung aktualisiert"
#: packages/lib/utils/document-audit-logs.ts:327
msgid "{prefix} updated the document external ID"
msgstr "{prefix} hat die externe ID des Dokuments aktualisiert"
#: packages/lib/utils/document-audit-logs.ts:311
msgid "{prefix} updated the document signing auth requirements"
msgstr "{prefix} hat die Authentifizierungsanforderungen für die Dokumentenunterzeichnung aktualisiert"
#: packages/lib/utils/document-audit-logs.ts:323
msgid "{prefix} updated the document title"
msgstr "{prefix} hat den Titel des Dokuments aktualisiert"
#: packages/lib/utils/document-audit-logs.ts:303
msgid "{prefix} updated the document visibility"
msgstr "{prefix} hat die Sichtbarkeit des Dokuments aktualisiert"
#: packages/email/templates/document-created-from-direct-template.tsx:55
msgid "{recipientName} {action} a document by using one of your direct links"
msgstr "{recipientName} {action} ein Dokument, indem Sie einen Ihrer direkten Links verwenden"
@ -104,6 +188,26 @@ msgstr "{recipientName} {action} ein Dokument, indem Sie einen Ihrer direkten Li
msgid "{teamName} ownership transfer request"
msgstr "Anfrage zur Übertragung des Eigentums von {teamName}"
#: packages/lib/utils/document-audit-logs.ts:343
msgid "{userName} approved the document"
msgstr "{userName} hat das Dokument genehmigt"
#: packages/lib/utils/document-audit-logs.ts:344
msgid "{userName} CC'd the document"
msgstr "{userName} hat das Dokument in CC gesetzt"
#: packages/lib/utils/document-audit-logs.ts:345
msgid "{userName} completed their task"
msgstr "{userName} hat ihre Aufgabe abgeschlossen"
#: packages/lib/utils/document-audit-logs.ts:341
msgid "{userName} signed the document"
msgstr "{userName} hat das Dokument unterschrieben"
#: packages/lib/utils/document-audit-logs.ts:342
msgid "{userName} viewed the document"
msgstr "{userName} hat das Dokument angesehen"
#: packages/ui/primitives/data-table-pagination.tsx:41
msgid "{visibleRows, plural, one {Showing # result.} other {Showing # results.}}"
msgstr "{visibleRows, plural, one {Eine # Ergebnis wird angezeigt.} other {# Ergebnisse werden angezeigt.}}"
@ -150,10 +254,34 @@ msgstr "<0>Passkey erforderlich</0> - Der Empfänger muss ein Konto haben und de
msgid "A document was created by your direct template that requires you to {recipientActionVerb} it."
msgstr "Ein Dokument wurde von deiner direkten Vorlage erstellt, das erfordert, dass du {recipientActionVerb}."
#: packages/lib/utils/document-audit-logs.ts:262
msgid "A field was added"
msgstr "Ein Feld wurde hinzugefügt"
#: packages/lib/utils/document-audit-logs.ts:266
msgid "A field was removed"
msgstr "Ein Feld wurde entfernt"
#: packages/lib/utils/document-audit-logs.ts:270
msgid "A field was updated"
msgstr "Ein Feld wurde aktualisiert"
#: packages/lib/jobs/definitions/emails/send-team-member-joined-email.ts:90
msgid "A new member has joined your team"
msgstr "Ein neues Mitglied ist deinem Team beigetreten"
#: packages/lib/utils/document-audit-logs.ts:274
msgid "A recipient was added"
msgstr "Ein Empfänger wurde hinzugefügt"
#: packages/lib/utils/document-audit-logs.ts:278
msgid "A recipient was removed"
msgstr "Ein Empfänger wurde entfernt"
#: packages/lib/utils/document-audit-logs.ts:282
msgid "A recipient was updated"
msgstr "Ein Empfänger wurde aktualisiert"
#: packages/lib/server-only/team/create-team-email-verification.ts:142
msgid "A request to use your email has been initiated by {teamName} on Documenso"
msgstr "Eine Anfrage zur Verwendung deiner E-Mail wurde von {teamName} auf Documenso initiiert"
@ -368,6 +496,7 @@ msgstr "Schließen"
#: packages/email/template-components/template-document-completed.tsx:35
#: packages/email/template-components/template-document-self-signed.tsx:36
#: packages/lib/constants/document.ts:10
msgid "Completed"
msgstr "Abgeschlossen"
@ -450,10 +579,24 @@ msgstr "Empfänger des direkten Links"
msgid "Document access"
msgstr "Dokumentenzugriff"
#: packages/lib/utils/document-audit-logs.ts:306
msgid "Document access auth updated"
msgstr "Die Authentifizierung für den Dokumentenzugriff wurde aktualisiert"
#: packages/lib/server-only/document/delete-document.ts:213
#: packages/lib/server-only/document/super-delete-document.ts:75
msgid "Document Cancelled"
msgstr "Dokument storniert"
#: packages/lib/utils/document-audit-logs.ts:359
#: packages/lib/utils/document-audit-logs.ts:360
msgid "Document completed"
msgstr "Dokument abgeschlossen"
#: packages/lib/utils/document-audit-logs.ts:286
msgid "Document created"
msgstr "Dokument erstellt"
#: packages/email/templates/document-created-from-direct-template.tsx:30
#: packages/lib/server-only/template/create-document-from-direct-template.ts:554
msgid "Document created from direct template"
@ -463,15 +606,55 @@ msgstr "Dokument erstellt aus direkter Vorlage"
msgid "Document Creation"
msgstr "Dokumenterstellung"
#: packages/lib/utils/document-audit-logs.ts:290
msgid "Document deleted"
msgstr "Dokument gelöscht"
#: packages/lib/server-only/document/send-delete-email.ts:58
msgid "Document Deleted!"
msgstr "Dokument gelöscht!"
#: packages/lib/utils/document-audit-logs.ts:326
msgid "Document external ID updated"
msgstr "Externe ID des Dokuments aktualisiert"
#: packages/lib/utils/document-audit-logs.ts:334
msgid "Document moved to team"
msgstr "Dokument ins Team verschoben"
#: packages/lib/utils/document-audit-logs.ts:318
msgid "Document opened"
msgstr "Dokument geöffnet"
#: packages/lib/utils/document-audit-logs.ts:330
msgid "Document sent"
msgstr "Dokument gesendet"
#: packages/lib/utils/document-audit-logs.ts:310
msgid "Document signing auth updated"
msgstr "Dokument unterzeichnen Authentifizierung aktualisiert"
#: packages/lib/utils/document-audit-logs.ts:322
msgid "Document title updated"
msgstr "Dokumenttitel aktualisiert"
#: packages/lib/utils/document-audit-logs.ts:314
msgid "Document updated"
msgstr "Dokument aktualisiert"
#: packages/lib/utils/document-audit-logs.ts:302
msgid "Document visibility updated"
msgstr "Sichtbarkeit des Dokuments aktualisiert"
#: packages/email/template-components/template-document-completed.tsx:64
#: packages/ui/components/document/document-download-button.tsx:68
msgid "Download"
msgstr "Herunterladen"
#: packages/lib/constants/document.ts:13
msgid "Draft"
msgstr "Entwurf"
#: packages/ui/primitives/document-dropzone.tsx:162
msgid "Drag & drop your PDF here."
msgstr "Ziehen Sie Ihr PDF hierher."
@ -504,6 +687,14 @@ msgstr "E-Mail ist erforderlich"
msgid "Email Options"
msgstr "E-Mail-Optionen"
#: packages/lib/utils/document-audit-logs.ts:353
msgid "Email resent"
msgstr "E-Mail erneut gesendet"
#: packages/lib/utils/document-audit-logs.ts:353
msgid "Email sent"
msgstr "E-Mail gesendet"
#: packages/ui/primitives/document-flow/add-fields.tsx:1123
msgid "Empty field"
msgstr "Leeres Feld"
@ -564,6 +755,14 @@ msgstr "Feldbeschriftung"
msgid "Field placeholder"
msgstr "Feldplatzhalter"
#: packages/lib/utils/document-audit-logs.ts:294
msgid "Field signed"
msgstr "Feld unterschrieben"
#: packages/lib/utils/document-audit-logs.ts:298
msgid "Field unsigned"
msgstr "Feld nicht unterschrieben"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/date-field.tsx:56
#: packages/ui/primitives/document-flow/field-items-advanced-settings/email-field.tsx:38
#: packages/ui/primitives/document-flow/field-items-advanced-settings/initials-field.tsx:38
@ -774,6 +973,10 @@ msgstr "Passwort erfolgreich zurückgesetzt"
msgid "Password updated!"
msgstr "Passwort aktualisiert!"
#: packages/lib/constants/document.ts:16
msgid "Pending"
msgstr "Ausstehend"
#: packages/email/templates/document-pending.tsx:17
msgid "Pending Document"
msgstr "Ausstehendes Dokument"
@ -841,6 +1044,10 @@ msgstr "Nur lesen"
msgid "Receives copy"
msgstr "Erhält Kopie"
#: packages/lib/utils/document-audit-logs.ts:338
msgid "Recipient"
msgstr "Empfänger"
#: packages/ui/components/recipient/recipient-action-auth-select.tsx:39
#: packages/ui/primitives/document-flow/add-settings.tsx:257
#: packages/ui/primitives/template-flow/add-template-settings.tsx:208
@ -1248,6 +1455,10 @@ msgstr "Wir haben dein Passwort wie gewünscht geändert. Du kannst dich jetzt m
msgid "Welcome to Documenso!"
msgstr "Willkommen bei Documenso!"
#: packages/lib/utils/document-audit-logs.ts:258
msgid "You"
msgstr "Du"
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:44
msgid "You are about to send this document to the recipients. Are you sure you want to continue?"
msgstr "Sie sind dabei, dieses Dokument an die Empfänger zu senden. Sind Sie sicher, dass Sie fortfahren möchten?"
@ -1309,4 +1520,3 @@ msgstr "Dein Passwort wurde aktualisiert."
#: packages/email/templates/team-delete.tsx:30
msgid "Your team has been deleted"
msgstr "Dein Team wurde gelöscht"

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: de\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-11-05 02:04\n"
"PO-Revision-Date: 2024-11-05 09:34\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@ -42,7 +42,7 @@ msgstr "Dokument hinzufügen"
msgid "Add More Users for {0}"
msgstr "Mehr Benutzer hinzufügen für {0}"
#: apps/marketing/src/app/(marketing)/open/page.tsx:164
#: apps/marketing/src/app/(marketing)/open/page.tsx:165
msgid "All our metrics, finances, and learnings are public. We believe in transparency and want to share our journey with you. You can read more about why here: <0>Announcing Open Metrics</0>"
msgstr "Alle unsere Kennzahlen, Finanzen und Erkenntnisse sind öffentlich. Wir glauben an Transparenz und möchten unsere Reise mit Ihnen teilen. Mehr erfahren Sie hier: <0>Ankündigung Offene Kennzahlen</0>"
@ -90,7 +90,7 @@ msgstr "Änderungsprotokoll"
msgid "Choose a template from the community app store. Or submit your own template for others to use."
msgstr "Wählen Sie eine Vorlage aus dem Community-App-Store. Oder reichen Sie Ihre eigene Vorlage ein, damit andere sie benutzen können."
#: apps/marketing/src/app/(marketing)/open/page.tsx:218
#: apps/marketing/src/app/(marketing)/open/page.tsx:219
msgid "Community"
msgstr "Gemeinschaft"
@ -193,7 +193,7 @@ msgstr "Schnell."
msgid "Faster, smarter and more beautiful."
msgstr "Schneller, intelligenter und schöner."
#: apps/marketing/src/app/(marketing)/open/page.tsx:209
#: apps/marketing/src/app/(marketing)/open/page.tsx:210
msgid "Finances"
msgstr "Finanzen"
@ -246,15 +246,15 @@ msgstr "Fangen Sie heute an."
msgid "Get the latest news from Documenso, including product updates, team announcements and more!"
msgstr "Erhalten Sie die neuesten Nachrichten von Documenso, einschließlich Produkt-Updates, Team-Ankündigungen und mehr!"
#: apps/marketing/src/app/(marketing)/open/page.tsx:232
#: apps/marketing/src/app/(marketing)/open/page.tsx:233
msgid "GitHub: Total Merged PRs"
msgstr "GitHub: Gesamte PRs zusammengeführt"
#: apps/marketing/src/app/(marketing)/open/page.tsx:250
#: apps/marketing/src/app/(marketing)/open/page.tsx:251
msgid "GitHub: Total Open Issues"
msgstr "GitHub: Gesamte offene Issues"
#: apps/marketing/src/app/(marketing)/open/page.tsx:224
#: apps/marketing/src/app/(marketing)/open/page.tsx:225
msgid "GitHub: Total Stars"
msgstr "GitHub: Gesamtanzahl Sterne"
@ -262,7 +262,7 @@ msgstr "GitHub: Gesamtanzahl Sterne"
msgid "Global Salary Bands"
msgstr "Globale Gehaltsbänder"
#: apps/marketing/src/app/(marketing)/open/page.tsx:260
#: apps/marketing/src/app/(marketing)/open/page.tsx:261
msgid "Growth"
msgstr "Wachstum"
@ -286,7 +286,7 @@ msgstr "Integrierte Zahlungen mit Stripe, sodass Sie sich keine Sorgen ums Bezah
msgid "Integrates with all your favourite tools."
msgstr "Integriert sich mit all Ihren Lieblingstools."
#: apps/marketing/src/app/(marketing)/open/page.tsx:288
#: apps/marketing/src/app/(marketing)/open/page.tsx:289
msgid "Is there more?"
msgstr "Gibt es mehr?"
@ -310,11 +310,11 @@ msgstr "Standort"
msgid "Make it your own through advanced customization and adjustability."
msgstr "Machen Sie es zu Ihrem eigenen durch erweiterte Anpassung und Einstellbarkeit."
#: apps/marketing/src/app/(marketing)/open/page.tsx:198
#: apps/marketing/src/app/(marketing)/open/page.tsx:199
msgid "Merged PR's"
msgstr "Zusammengeführte PRs"
#: apps/marketing/src/app/(marketing)/open/page.tsx:233
#: apps/marketing/src/app/(marketing)/open/page.tsx:234
msgid "Merged PRs"
msgstr "Zusammengeführte PRs"
@ -345,8 +345,8 @@ msgstr "Keine Kreditkarte erforderlich"
msgid "None of these work for you? Try self-hosting!"
msgstr "Keines dieser Angebote passt zu Ihnen? Versuchen Sie das Selbst-Hosting!"
#: apps/marketing/src/app/(marketing)/open/page.tsx:193
#: apps/marketing/src/app/(marketing)/open/page.tsx:251
#: apps/marketing/src/app/(marketing)/open/page.tsx:194
#: apps/marketing/src/app/(marketing)/open/page.tsx:252
msgid "Open Issues"
msgstr "Offene Issues"
@ -354,7 +354,7 @@ msgstr "Offene Issues"
msgid "Open Source or Hosted."
msgstr "Open Source oder Hosted."
#: apps/marketing/src/app/(marketing)/open/page.tsx:160
#: apps/marketing/src/app/(marketing)/open/page.tsx:161
#: apps/marketing/src/components/(marketing)/footer.tsx:37
#: apps/marketing/src/components/(marketing)/header.tsx:64
#: apps/marketing/src/components/(marketing)/mobile-navigation.tsx:40
@ -466,7 +466,7 @@ msgstr "Intelligent."
msgid "Star on GitHub"
msgstr "Auf GitHub favorisieren"
#: apps/marketing/src/app/(marketing)/open/page.tsx:225
#: apps/marketing/src/app/(marketing)/open/page.tsx:226
msgid "Stars"
msgstr "Favoriten"
@ -501,7 +501,7 @@ msgstr "Vorlagen-Shop (Demnächst)."
msgid "That's awesome. You can take a look at the current <0>Issues</0> and join our <1>Discord Community</1> to keep up to date, on what the current priorities are. In any case, we are an open community and welcome all input, technical and non-technical ❤️"
msgstr "Das ist großartig. Sie können sich die aktuellen <0>Issues</0> ansehen und unserer <1>Discord-Community</1> beitreten, um auf dem neuesten Stand zu bleiben, was die aktuellen Prioritäten sind. In jedem Fall sind wir eine offene Gemeinschaft und begrüßen jegliche Beiträge, technische und nicht-technische ❤️"
#: apps/marketing/src/app/(marketing)/open/page.tsx:292
#: apps/marketing/src/app/(marketing)/open/page.tsx:293
msgid "This page is evolving as we learn what makes a great signing company. We'll update it when we have more to share."
msgstr "Diese Seite entwickelt sich weiter, während wir lernen, was ein großartiges Signing-Unternehmen ausmacht. Wir werden sie aktualisieren, wenn wir mehr zu teilen haben."
@ -514,8 +514,8 @@ msgstr "Titel"
msgid "Total Completed Documents"
msgstr "Insgesamt Abgeschlossene Dokumente"
#: apps/marketing/src/app/(marketing)/open/page.tsx:266
#: apps/marketing/src/app/(marketing)/open/page.tsx:267
#: apps/marketing/src/app/(marketing)/open/page.tsx:268
msgid "Total Customers"
msgstr "Insgesamt Kunden"
@ -602,4 +602,3 @@ msgstr "Sie können Documenso kostenlos selbst hosten oder unsere sofort einsatz
#: apps/marketing/src/components/(marketing)/carousel.tsx:272
msgid "Your browser does not support the video tag."
msgstr "Ihr Browser unterstützt das Video-Tag nicht."

File diff suppressed because it is too large Load Diff

View File

@ -91,6 +91,90 @@ msgstr "{memberEmail} joined the following team"
msgid "{memberEmail} left the following team"
msgstr "{memberEmail} left the following team"
#: packages/lib/utils/document-audit-logs.ts:263
msgid "{prefix} added a field"
msgstr "{prefix} added a field"
#: packages/lib/utils/document-audit-logs.ts:275
msgid "{prefix} added a recipient"
msgstr "{prefix} added a recipient"
#: packages/lib/utils/document-audit-logs.ts:287
msgid "{prefix} created the document"
msgstr "{prefix} created the document"
#: packages/lib/utils/document-audit-logs.ts:291
msgid "{prefix} deleted the document"
msgstr "{prefix} deleted the document"
#: packages/lib/utils/document-audit-logs.ts:335
msgid "{prefix} moved the document to team"
msgstr "{prefix} moved the document to team"
#: packages/lib/utils/document-audit-logs.ts:319
msgid "{prefix} opened the document"
msgstr "{prefix} opened the document"
#: packages/lib/utils/document-audit-logs.ts:267
msgid "{prefix} removed a field"
msgstr "{prefix} removed a field"
#: packages/lib/utils/document-audit-logs.ts:279
msgid "{prefix} removed a recipient"
msgstr "{prefix} removed a recipient"
#: packages/lib/utils/document-audit-logs.ts:355
msgid "{prefix} resent an email to {0}"
msgstr "{prefix} resent an email to {0}"
#: packages/lib/utils/document-audit-logs.ts:356
msgid "{prefix} sent an email to {0}"
msgstr "{prefix} sent an email to {0}"
#: packages/lib/utils/document-audit-logs.ts:331
msgid "{prefix} sent the document"
msgstr "{prefix} sent the document"
#: packages/lib/utils/document-audit-logs.ts:295
msgid "{prefix} signed a field"
msgstr "{prefix} signed a field"
#: packages/lib/utils/document-audit-logs.ts:299
msgid "{prefix} unsigned a field"
msgstr "{prefix} unsigned a field"
#: packages/lib/utils/document-audit-logs.ts:271
msgid "{prefix} updated a field"
msgstr "{prefix} updated a field"
#: packages/lib/utils/document-audit-logs.ts:283
msgid "{prefix} updated a recipient"
msgstr "{prefix} updated a recipient"
#: packages/lib/utils/document-audit-logs.ts:315
msgid "{prefix} updated the document"
msgstr "{prefix} updated the document"
#: packages/lib/utils/document-audit-logs.ts:307
msgid "{prefix} updated the document access auth requirements"
msgstr "{prefix} updated the document access auth requirements"
#: packages/lib/utils/document-audit-logs.ts:327
msgid "{prefix} updated the document external ID"
msgstr "{prefix} updated the document external ID"
#: packages/lib/utils/document-audit-logs.ts:311
msgid "{prefix} updated the document signing auth requirements"
msgstr "{prefix} updated the document signing auth requirements"
#: packages/lib/utils/document-audit-logs.ts:323
msgid "{prefix} updated the document title"
msgstr "{prefix} updated the document title"
#: packages/lib/utils/document-audit-logs.ts:303
msgid "{prefix} updated the document visibility"
msgstr "{prefix} updated the document visibility"
#: packages/email/templates/document-created-from-direct-template.tsx:55
msgid "{recipientName} {action} a document by using one of your direct links"
msgstr "{recipientName} {action} a document by using one of your direct links"
@ -99,6 +183,26 @@ msgstr "{recipientName} {action} a document by using one of your direct links"
msgid "{teamName} ownership transfer request"
msgstr "{teamName} ownership transfer request"
#: packages/lib/utils/document-audit-logs.ts:343
msgid "{userName} approved the document"
msgstr "{userName} approved the document"
#: packages/lib/utils/document-audit-logs.ts:344
msgid "{userName} CC'd the document"
msgstr "{userName} CC'd the document"
#: packages/lib/utils/document-audit-logs.ts:345
msgid "{userName} completed their task"
msgstr "{userName} completed their task"
#: packages/lib/utils/document-audit-logs.ts:341
msgid "{userName} signed the document"
msgstr "{userName} signed the document"
#: packages/lib/utils/document-audit-logs.ts:342
msgid "{userName} viewed the document"
msgstr "{userName} viewed the document"
#: packages/ui/primitives/data-table-pagination.tsx:41
msgid "{visibleRows, plural, one {Showing # result.} other {Showing # results.}}"
msgstr "{visibleRows, plural, one {Showing # result.} other {Showing # results.}}"
@ -145,10 +249,34 @@ msgstr "<0>Require passkey</0> - The recipient must have an account and passkey
msgid "A document was created by your direct template that requires you to {recipientActionVerb} it."
msgstr "A document was created by your direct template that requires you to {recipientActionVerb} it."
#: packages/lib/utils/document-audit-logs.ts:262
msgid "A field was added"
msgstr "A field was added"
#: packages/lib/utils/document-audit-logs.ts:266
msgid "A field was removed"
msgstr "A field was removed"
#: packages/lib/utils/document-audit-logs.ts:270
msgid "A field was updated"
msgstr "A field was updated"
#: packages/lib/jobs/definitions/emails/send-team-member-joined-email.ts:90
msgid "A new member has joined your team"
msgstr "A new member has joined your team"
#: packages/lib/utils/document-audit-logs.ts:274
msgid "A recipient was added"
msgstr "A recipient was added"
#: packages/lib/utils/document-audit-logs.ts:278
msgid "A recipient was removed"
msgstr "A recipient was removed"
#: packages/lib/utils/document-audit-logs.ts:282
msgid "A recipient was updated"
msgstr "A recipient was updated"
#: packages/lib/server-only/team/create-team-email-verification.ts:142
msgid "A request to use your email has been initiated by {teamName} on Documenso"
msgstr "A request to use your email has been initiated by {teamName} on Documenso"
@ -363,6 +491,7 @@ msgstr "Close"
#: packages/email/template-components/template-document-completed.tsx:35
#: packages/email/template-components/template-document-self-signed.tsx:36
#: packages/lib/constants/document.ts:10
msgid "Completed"
msgstr "Completed"
@ -445,10 +574,24 @@ msgstr "Direct link receiver"
msgid "Document access"
msgstr "Document access"
#: packages/lib/utils/document-audit-logs.ts:306
msgid "Document access auth updated"
msgstr "Document access auth updated"
#: packages/lib/server-only/document/delete-document.ts:213
#: packages/lib/server-only/document/super-delete-document.ts:75
msgid "Document Cancelled"
msgstr "Document Cancelled"
#: packages/lib/utils/document-audit-logs.ts:359
#: packages/lib/utils/document-audit-logs.ts:360
msgid "Document completed"
msgstr "Document completed"
#: packages/lib/utils/document-audit-logs.ts:286
msgid "Document created"
msgstr "Document created"
#: packages/email/templates/document-created-from-direct-template.tsx:30
#: packages/lib/server-only/template/create-document-from-direct-template.ts:554
msgid "Document created from direct template"
@ -458,15 +601,55 @@ msgstr "Document created from direct template"
msgid "Document Creation"
msgstr "Document Creation"
#: packages/lib/utils/document-audit-logs.ts:290
msgid "Document deleted"
msgstr "Document deleted"
#: packages/lib/server-only/document/send-delete-email.ts:58
msgid "Document Deleted!"
msgstr "Document Deleted!"
#: packages/lib/utils/document-audit-logs.ts:326
msgid "Document external ID updated"
msgstr "Document external ID updated"
#: packages/lib/utils/document-audit-logs.ts:334
msgid "Document moved to team"
msgstr "Document moved to team"
#: packages/lib/utils/document-audit-logs.ts:318
msgid "Document opened"
msgstr "Document opened"
#: packages/lib/utils/document-audit-logs.ts:330
msgid "Document sent"
msgstr "Document sent"
#: packages/lib/utils/document-audit-logs.ts:310
msgid "Document signing auth updated"
msgstr "Document signing auth updated"
#: packages/lib/utils/document-audit-logs.ts:322
msgid "Document title updated"
msgstr "Document title updated"
#: packages/lib/utils/document-audit-logs.ts:314
msgid "Document updated"
msgstr "Document updated"
#: packages/lib/utils/document-audit-logs.ts:302
msgid "Document visibility updated"
msgstr "Document visibility updated"
#: packages/email/template-components/template-document-completed.tsx:64
#: packages/ui/components/document/document-download-button.tsx:68
msgid "Download"
msgstr "Download"
#: packages/lib/constants/document.ts:13
msgid "Draft"
msgstr "Draft"
#: packages/ui/primitives/document-dropzone.tsx:162
msgid "Drag & drop your PDF here."
msgstr "Drag & drop your PDF here."
@ -499,6 +682,14 @@ msgstr "Email is required"
msgid "Email Options"
msgstr "Email Options"
#: packages/lib/utils/document-audit-logs.ts:353
msgid "Email resent"
msgstr "Email resent"
#: packages/lib/utils/document-audit-logs.ts:353
msgid "Email sent"
msgstr "Email sent"
#: packages/ui/primitives/document-flow/add-fields.tsx:1123
msgid "Empty field"
msgstr "Empty field"
@ -559,6 +750,14 @@ msgstr "Field label"
msgid "Field placeholder"
msgstr "Field placeholder"
#: packages/lib/utils/document-audit-logs.ts:294
msgid "Field signed"
msgstr "Field signed"
#: packages/lib/utils/document-audit-logs.ts:298
msgid "Field unsigned"
msgstr "Field unsigned"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/date-field.tsx:56
#: packages/ui/primitives/document-flow/field-items-advanced-settings/email-field.tsx:38
#: packages/ui/primitives/document-flow/field-items-advanced-settings/initials-field.tsx:38
@ -769,6 +968,10 @@ msgstr "Password Reset Successful"
msgid "Password updated!"
msgstr "Password updated!"
#: packages/lib/constants/document.ts:16
msgid "Pending"
msgstr "Pending"
#: packages/email/templates/document-pending.tsx:17
msgid "Pending Document"
msgstr "Pending Document"
@ -836,6 +1039,10 @@ msgstr "Read only"
msgid "Receives copy"
msgstr "Receives copy"
#: packages/lib/utils/document-audit-logs.ts:338
msgid "Recipient"
msgstr "Recipient"
#: packages/ui/components/recipient/recipient-action-auth-select.tsx:39
#: packages/ui/primitives/document-flow/add-settings.tsx:257
#: packages/ui/primitives/template-flow/add-template-settings.tsx:208
@ -1243,6 +1450,10 @@ msgstr "We've changed your password as you asked. You can now sign in with your
msgid "Welcome to Documenso!"
msgstr "Welcome to Documenso!"
#: packages/lib/utils/document-audit-logs.ts:258
msgid "You"
msgstr "You"
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:44
msgid "You are about to send this document to the recipients. Are you sure you want to continue?"
msgstr "You are about to send this document to the recipients. Are you sure you want to continue?"

View File

@ -37,7 +37,7 @@ msgstr "Add document"
msgid "Add More Users for {0}"
msgstr "Add More Users for {0}"
#: apps/marketing/src/app/(marketing)/open/page.tsx:164
#: apps/marketing/src/app/(marketing)/open/page.tsx:165
msgid "All our metrics, finances, and learnings are public. We believe in transparency and want to share our journey with you. You can read more about why here: <0>Announcing Open Metrics</0>"
msgstr "All our metrics, finances, and learnings are public. We believe in transparency and want to share our journey with you. You can read more about why here: <0>Announcing Open Metrics</0>"
@ -85,7 +85,7 @@ msgstr "Changelog"
msgid "Choose a template from the community app store. Or submit your own template for others to use."
msgstr "Choose a template from the community app store. Or submit your own template for others to use."
#: apps/marketing/src/app/(marketing)/open/page.tsx:218
#: apps/marketing/src/app/(marketing)/open/page.tsx:219
msgid "Community"
msgstr "Community"
@ -188,7 +188,7 @@ msgstr "Fast."
msgid "Faster, smarter and more beautiful."
msgstr "Faster, smarter and more beautiful."
#: apps/marketing/src/app/(marketing)/open/page.tsx:209
#: apps/marketing/src/app/(marketing)/open/page.tsx:210
msgid "Finances"
msgstr "Finances"
@ -241,15 +241,15 @@ msgstr "Get started today."
msgid "Get the latest news from Documenso, including product updates, team announcements and more!"
msgstr "Get the latest news from Documenso, including product updates, team announcements and more!"
#: apps/marketing/src/app/(marketing)/open/page.tsx:232
#: apps/marketing/src/app/(marketing)/open/page.tsx:233
msgid "GitHub: Total Merged PRs"
msgstr "GitHub: Total Merged PRs"
#: apps/marketing/src/app/(marketing)/open/page.tsx:250
#: apps/marketing/src/app/(marketing)/open/page.tsx:251
msgid "GitHub: Total Open Issues"
msgstr "GitHub: Total Open Issues"
#: apps/marketing/src/app/(marketing)/open/page.tsx:224
#: apps/marketing/src/app/(marketing)/open/page.tsx:225
msgid "GitHub: Total Stars"
msgstr "GitHub: Total Stars"
@ -257,7 +257,7 @@ msgstr "GitHub: Total Stars"
msgid "Global Salary Bands"
msgstr "Global Salary Bands"
#: apps/marketing/src/app/(marketing)/open/page.tsx:260
#: apps/marketing/src/app/(marketing)/open/page.tsx:261
msgid "Growth"
msgstr "Growth"
@ -281,7 +281,7 @@ msgstr "Integrated payments with Stripe so you dont have to worry about getti
msgid "Integrates with all your favourite tools."
msgstr "Integrates with all your favourite tools."
#: apps/marketing/src/app/(marketing)/open/page.tsx:288
#: apps/marketing/src/app/(marketing)/open/page.tsx:289
msgid "Is there more?"
msgstr "Is there more?"
@ -305,11 +305,11 @@ msgstr "Location"
msgid "Make it your own through advanced customization and adjustability."
msgstr "Make it your own through advanced customization and adjustability."
#: apps/marketing/src/app/(marketing)/open/page.tsx:198
#: apps/marketing/src/app/(marketing)/open/page.tsx:199
msgid "Merged PR's"
msgstr "Merged PR's"
#: apps/marketing/src/app/(marketing)/open/page.tsx:233
#: apps/marketing/src/app/(marketing)/open/page.tsx:234
msgid "Merged PRs"
msgstr "Merged PRs"
@ -340,8 +340,8 @@ msgstr "No Credit Card required"
msgid "None of these work for you? Try self-hosting!"
msgstr "None of these work for you? Try self-hosting!"
#: apps/marketing/src/app/(marketing)/open/page.tsx:193
#: apps/marketing/src/app/(marketing)/open/page.tsx:251
#: apps/marketing/src/app/(marketing)/open/page.tsx:194
#: apps/marketing/src/app/(marketing)/open/page.tsx:252
msgid "Open Issues"
msgstr "Open Issues"
@ -349,7 +349,7 @@ msgstr "Open Issues"
msgid "Open Source or Hosted."
msgstr "Open Source or Hosted."
#: apps/marketing/src/app/(marketing)/open/page.tsx:160
#: apps/marketing/src/app/(marketing)/open/page.tsx:161
#: apps/marketing/src/components/(marketing)/footer.tsx:37
#: apps/marketing/src/components/(marketing)/header.tsx:64
#: apps/marketing/src/components/(marketing)/mobile-navigation.tsx:40
@ -461,7 +461,7 @@ msgstr "Smart."
msgid "Star on GitHub"
msgstr "Star on GitHub"
#: apps/marketing/src/app/(marketing)/open/page.tsx:225
#: apps/marketing/src/app/(marketing)/open/page.tsx:226
msgid "Stars"
msgstr "Stars"
@ -496,7 +496,7 @@ msgstr "Template Store (Soon)."
msgid "That's awesome. You can take a look at the current <0>Issues</0> and join our <1>Discord Community</1> to keep up to date, on what the current priorities are. In any case, we are an open community and welcome all input, technical and non-technical ❤️"
msgstr "That's awesome. You can take a look at the current <0>Issues</0> and join our <1>Discord Community</1> to keep up to date, on what the current priorities are. In any case, we are an open community and welcome all input, technical and non-technical ❤️"
#: apps/marketing/src/app/(marketing)/open/page.tsx:292
#: apps/marketing/src/app/(marketing)/open/page.tsx:293
msgid "This page is evolving as we learn what makes a great signing company. We'll update it when we have more to share."
msgstr "This page is evolving as we learn what makes a great signing company. We'll update it when we have more to share."
@ -509,8 +509,8 @@ msgstr "Title"
msgid "Total Completed Documents"
msgstr "Total Completed Documents"
#: apps/marketing/src/app/(marketing)/open/page.tsx:266
#: apps/marketing/src/app/(marketing)/open/page.tsx:267
#: apps/marketing/src/app/(marketing)/open/page.tsx:268
msgid "Total Customers"
msgstr "Total Customers"

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: es\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-11-05 02:04\n"
"PO-Revision-Date: 2024-11-05 09:34\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@ -96,6 +96,90 @@ msgstr "{memberEmail} se unió al siguiente equipo"
msgid "{memberEmail} left the following team"
msgstr "{memberEmail} dejó el siguiente equipo"
#: packages/lib/utils/document-audit-logs.ts:263
msgid "{prefix} added a field"
msgstr "{prefix} agregó un campo"
#: packages/lib/utils/document-audit-logs.ts:275
msgid "{prefix} added a recipient"
msgstr "{prefix} agregó un destinatario"
#: packages/lib/utils/document-audit-logs.ts:287
msgid "{prefix} created the document"
msgstr "{prefix} creó el documento"
#: packages/lib/utils/document-audit-logs.ts:291
msgid "{prefix} deleted the document"
msgstr "{prefix} eliminó el documento"
#: packages/lib/utils/document-audit-logs.ts:335
msgid "{prefix} moved the document to team"
msgstr "{prefix} movió el documento al equipo"
#: packages/lib/utils/document-audit-logs.ts:319
msgid "{prefix} opened the document"
msgstr "{prefix} abrió el documento"
#: packages/lib/utils/document-audit-logs.ts:267
msgid "{prefix} removed a field"
msgstr "{prefix} eliminó un campo"
#: packages/lib/utils/document-audit-logs.ts:279
msgid "{prefix} removed a recipient"
msgstr "{prefix} eliminó un destinatario"
#: packages/lib/utils/document-audit-logs.ts:355
msgid "{prefix} resent an email to {0}"
msgstr "{prefix} reenviaron un correo electrónico a {0}"
#: packages/lib/utils/document-audit-logs.ts:356
msgid "{prefix} sent an email to {0}"
msgstr "{prefix} envió un correo electrónico a {0}"
#: packages/lib/utils/document-audit-logs.ts:331
msgid "{prefix} sent the document"
msgstr "{prefix} envió el documento"
#: packages/lib/utils/document-audit-logs.ts:295
msgid "{prefix} signed a field"
msgstr "{prefix} firmó un campo"
#: packages/lib/utils/document-audit-logs.ts:299
msgid "{prefix} unsigned a field"
msgstr "{prefix} no firmó un campo"
#: packages/lib/utils/document-audit-logs.ts:271
msgid "{prefix} updated a field"
msgstr "{prefix} actualizó un campo"
#: packages/lib/utils/document-audit-logs.ts:283
msgid "{prefix} updated a recipient"
msgstr "{prefix} actualizó un destinatario"
#: packages/lib/utils/document-audit-logs.ts:315
msgid "{prefix} updated the document"
msgstr "{prefix} actualizó el documento"
#: packages/lib/utils/document-audit-logs.ts:307
msgid "{prefix} updated the document access auth requirements"
msgstr "{prefix} actualizó los requisitos de autorización de acceso al documento"
#: packages/lib/utils/document-audit-logs.ts:327
msgid "{prefix} updated the document external ID"
msgstr "{prefix} actualizó el ID externo del documento"
#: packages/lib/utils/document-audit-logs.ts:311
msgid "{prefix} updated the document signing auth requirements"
msgstr "{prefix} actualizó los requisitos de autenticación para la firma del documento"
#: packages/lib/utils/document-audit-logs.ts:323
msgid "{prefix} updated the document title"
msgstr "{prefix} actualizó el título del documento"
#: packages/lib/utils/document-audit-logs.ts:303
msgid "{prefix} updated the document visibility"
msgstr "{prefix} actualizó la visibilidad del documento"
#: packages/email/templates/document-created-from-direct-template.tsx:55
msgid "{recipientName} {action} a document by using one of your direct links"
msgstr "{recipientName} {action} un documento utilizando uno de tus enlaces directos"
@ -104,6 +188,26 @@ msgstr "{recipientName} {action} un documento utilizando uno de tus enlaces dire
msgid "{teamName} ownership transfer request"
msgstr "solicitud de transferencia de propiedad de {teamName}"
#: packages/lib/utils/document-audit-logs.ts:343
msgid "{userName} approved the document"
msgstr "{userName} aprobó el documento"
#: packages/lib/utils/document-audit-logs.ts:344
msgid "{userName} CC'd the document"
msgstr "{userName} envió una copia del documento"
#: packages/lib/utils/document-audit-logs.ts:345
msgid "{userName} completed their task"
msgstr "{userName} completó su tarea"
#: packages/lib/utils/document-audit-logs.ts:341
msgid "{userName} signed the document"
msgstr "{userName} firmó el documento"
#: packages/lib/utils/document-audit-logs.ts:342
msgid "{userName} viewed the document"
msgstr "{userName} vio el documento"
#: packages/ui/primitives/data-table-pagination.tsx:41
msgid "{visibleRows, plural, one {Showing # result.} other {Showing # results.}}"
msgstr "{visibleRows, plural, one {Mostrando # resultado.} other {Mostrando # resultados.}}"
@ -150,10 +254,34 @@ msgstr "<0>Requerir clave de acceso</0> - El destinatario debe tener una cuenta
msgid "A document was created by your direct template that requires you to {recipientActionVerb} it."
msgstr "Se creó un documento a partir de tu plantilla directa que requiere que {recipientActionVerb}."
#: packages/lib/utils/document-audit-logs.ts:262
msgid "A field was added"
msgstr "Se añadió un campo"
#: packages/lib/utils/document-audit-logs.ts:266
msgid "A field was removed"
msgstr "Se eliminó un campo"
#: packages/lib/utils/document-audit-logs.ts:270
msgid "A field was updated"
msgstr "Se actualizó un campo"
#: packages/lib/jobs/definitions/emails/send-team-member-joined-email.ts:90
msgid "A new member has joined your team"
msgstr "Un nuevo miembro se ha unido a tu equipo"
#: packages/lib/utils/document-audit-logs.ts:274
msgid "A recipient was added"
msgstr "Se añadió un destinatario"
#: packages/lib/utils/document-audit-logs.ts:278
msgid "A recipient was removed"
msgstr "Se eliminó un destinatario"
#: packages/lib/utils/document-audit-logs.ts:282
msgid "A recipient was updated"
msgstr "Se actualizó un destinatario"
#: packages/lib/server-only/team/create-team-email-verification.ts:142
msgid "A request to use your email has been initiated by {teamName} on Documenso"
msgstr "Se ha iniciado una solicitud para utilizar tu correo electrónico por {teamName} en Documenso"
@ -368,6 +496,7 @@ msgstr "Cerrar"
#: packages/email/template-components/template-document-completed.tsx:35
#: packages/email/template-components/template-document-self-signed.tsx:36
#: packages/lib/constants/document.ts:10
msgid "Completed"
msgstr "Completado"
@ -450,10 +579,24 @@ msgstr "Receptor de enlace directo"
msgid "Document access"
msgstr "Acceso al documento"
#: packages/lib/utils/document-audit-logs.ts:306
msgid "Document access auth updated"
msgstr "Se actualizó la autenticación de acceso al documento"
#: packages/lib/server-only/document/delete-document.ts:213
#: packages/lib/server-only/document/super-delete-document.ts:75
msgid "Document Cancelled"
msgstr "Documento cancelado"
#: packages/lib/utils/document-audit-logs.ts:359
#: packages/lib/utils/document-audit-logs.ts:360
msgid "Document completed"
msgstr "Documento completado"
#: packages/lib/utils/document-audit-logs.ts:286
msgid "Document created"
msgstr "Documento creado"
#: packages/email/templates/document-created-from-direct-template.tsx:30
#: packages/lib/server-only/template/create-document-from-direct-template.ts:554
msgid "Document created from direct template"
@ -463,15 +606,55 @@ msgstr "Documento creado a partir de plantilla directa"
msgid "Document Creation"
msgstr "Creación de documento"
#: packages/lib/utils/document-audit-logs.ts:290
msgid "Document deleted"
msgstr "Documento eliminado"
#: packages/lib/server-only/document/send-delete-email.ts:58
msgid "Document Deleted!"
msgstr "¡Documento eliminado!"
#: packages/lib/utils/document-audit-logs.ts:326
msgid "Document external ID updated"
msgstr "ID externo del documento actualizado"
#: packages/lib/utils/document-audit-logs.ts:334
msgid "Document moved to team"
msgstr "Documento movido al equipo"
#: packages/lib/utils/document-audit-logs.ts:318
msgid "Document opened"
msgstr "Documento abierto"
#: packages/lib/utils/document-audit-logs.ts:330
msgid "Document sent"
msgstr "Documento enviado"
#: packages/lib/utils/document-audit-logs.ts:310
msgid "Document signing auth updated"
msgstr "Se actualizó la autenticación de firma del documento"
#: packages/lib/utils/document-audit-logs.ts:322
msgid "Document title updated"
msgstr "Título del documento actualizado"
#: packages/lib/utils/document-audit-logs.ts:314
msgid "Document updated"
msgstr "Documento actualizado"
#: packages/lib/utils/document-audit-logs.ts:302
msgid "Document visibility updated"
msgstr "Visibilidad del documento actualizada"
#: packages/email/template-components/template-document-completed.tsx:64
#: packages/ui/components/document/document-download-button.tsx:68
msgid "Download"
msgstr "Descargar"
#: packages/lib/constants/document.ts:13
msgid "Draft"
msgstr "Borrador"
#: packages/ui/primitives/document-dropzone.tsx:162
msgid "Drag & drop your PDF here."
msgstr "Arrastre y suelte su PDF aquí."
@ -504,6 +687,14 @@ msgstr "Se requiere email"
msgid "Email Options"
msgstr "Opciones de correo electrónico"
#: packages/lib/utils/document-audit-logs.ts:353
msgid "Email resent"
msgstr "Correo electrónico reeenviado"
#: packages/lib/utils/document-audit-logs.ts:353
msgid "Email sent"
msgstr "Correo electrónico enviado"
#: packages/ui/primitives/document-flow/add-fields.tsx:1123
msgid "Empty field"
msgstr "Campo vacío"
@ -564,6 +755,14 @@ msgstr "Etiqueta de campo"
msgid "Field placeholder"
msgstr "Marcador de posición de campo"
#: packages/lib/utils/document-audit-logs.ts:294
msgid "Field signed"
msgstr "Campo firmado"
#: packages/lib/utils/document-audit-logs.ts:298
msgid "Field unsigned"
msgstr "Campo no firmado"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/date-field.tsx:56
#: packages/ui/primitives/document-flow/field-items-advanced-settings/email-field.tsx:38
#: packages/ui/primitives/document-flow/field-items-advanced-settings/initials-field.tsx:38
@ -774,6 +973,10 @@ msgstr "Restablecimiento de contraseña exitoso"
msgid "Password updated!"
msgstr "¡Contraseña actualizada!"
#: packages/lib/constants/document.ts:16
msgid "Pending"
msgstr "Pendiente"
#: packages/email/templates/document-pending.tsx:17
msgid "Pending Document"
msgstr "Documento pendiente"
@ -841,6 +1044,10 @@ msgstr "Solo lectura"
msgid "Receives copy"
msgstr "Recibe copia"
#: packages/lib/utils/document-audit-logs.ts:338
msgid "Recipient"
msgstr "Destinatario"
#: packages/ui/components/recipient/recipient-action-auth-select.tsx:39
#: packages/ui/primitives/document-flow/add-settings.tsx:257
#: packages/ui/primitives/template-flow/add-template-settings.tsx:208
@ -1248,6 +1455,10 @@ msgstr "Hemos cambiado tu contraseña como solicitaste. Ahora puedes iniciar ses
msgid "Welcome to Documenso!"
msgstr "¡Bienvenido a Documenso!"
#: packages/lib/utils/document-audit-logs.ts:258
msgid "You"
msgstr "Tú"
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:44
msgid "You are about to send this document to the recipients. Are you sure you want to continue?"
msgstr "Está a punto de enviar este documento a los destinatarios. ¿Está seguro de que desea continuar?"
@ -1309,4 +1520,3 @@ msgstr "Tu contraseña ha sido actualizada."
#: packages/email/templates/team-delete.tsx:30
msgid "Your team has been deleted"
msgstr "Tu equipo ha sido eliminado"

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: es\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-11-05 02:04\n"
"PO-Revision-Date: 2024-11-05 09:34\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@ -42,7 +42,7 @@ msgstr "Agregar documento"
msgid "Add More Users for {0}"
msgstr "Agregar más usuarios por {0}"
#: apps/marketing/src/app/(marketing)/open/page.tsx:164
#: apps/marketing/src/app/(marketing)/open/page.tsx:165
msgid "All our metrics, finances, and learnings are public. We believe in transparency and want to share our journey with you. You can read more about why here: <0>Announcing Open Metrics</0>"
msgstr "Todos nuestros métricas, finanzas y aprendizajes son públicos. Creemos en la transparencia y queremos compartir nuestro viaje contigo. Puedes leer más sobre por qué aquí: <0>Anunciando Métricas Abiertas</0>"
@ -90,7 +90,7 @@ msgstr "Registro de cambios"
msgid "Choose a template from the community app store. Or submit your own template for others to use."
msgstr "Elige una plantilla de la tienda de aplicaciones de la comunidad. O envía tu propia plantilla para que otros la usen."
#: apps/marketing/src/app/(marketing)/open/page.tsx:218
#: apps/marketing/src/app/(marketing)/open/page.tsx:219
msgid "Community"
msgstr "Comunidad"
@ -193,7 +193,7 @@ msgstr "Rápido."
msgid "Faster, smarter and more beautiful."
msgstr "Más rápido, más inteligente y más hermoso."
#: apps/marketing/src/app/(marketing)/open/page.tsx:209
#: apps/marketing/src/app/(marketing)/open/page.tsx:210
msgid "Finances"
msgstr "Finanzas"
@ -246,15 +246,15 @@ msgstr "Comienza hoy."
msgid "Get the latest news from Documenso, including product updates, team announcements and more!"
msgstr "¡Obtén las últimas noticias de Documenso, incluidas actualizaciones de productos, anuncios del equipo y más!"
#: apps/marketing/src/app/(marketing)/open/page.tsx:232
#: apps/marketing/src/app/(marketing)/open/page.tsx:233
msgid "GitHub: Total Merged PRs"
msgstr "GitHub: Total de PRs fusionados"
#: apps/marketing/src/app/(marketing)/open/page.tsx:250
#: apps/marketing/src/app/(marketing)/open/page.tsx:251
msgid "GitHub: Total Open Issues"
msgstr "GitHub: Total de problemas abiertos"
#: apps/marketing/src/app/(marketing)/open/page.tsx:224
#: apps/marketing/src/app/(marketing)/open/page.tsx:225
msgid "GitHub: Total Stars"
msgstr "GitHub: Total de estrellas"
@ -262,7 +262,7 @@ msgstr "GitHub: Total de estrellas"
msgid "Global Salary Bands"
msgstr "Bandas salariales globales"
#: apps/marketing/src/app/(marketing)/open/page.tsx:260
#: apps/marketing/src/app/(marketing)/open/page.tsx:261
msgid "Growth"
msgstr "Crecimiento"
@ -286,7 +286,7 @@ msgstr "Pagos integrados con Stripe para que no tengas que preocuparte por cobra
msgid "Integrates with all your favourite tools."
msgstr "Se integra con todas tus herramientas favoritas."
#: apps/marketing/src/app/(marketing)/open/page.tsx:288
#: apps/marketing/src/app/(marketing)/open/page.tsx:289
msgid "Is there more?"
msgstr "¿Hay más?"
@ -310,11 +310,11 @@ msgstr "Ubicación"
msgid "Make it your own through advanced customization and adjustability."
msgstr "Hazlo tuyo a través de personalización y ajustabilidad avanzadas."
#: apps/marketing/src/app/(marketing)/open/page.tsx:198
#: apps/marketing/src/app/(marketing)/open/page.tsx:199
msgid "Merged PR's"
msgstr "PRs fusionados"
#: apps/marketing/src/app/(marketing)/open/page.tsx:233
#: apps/marketing/src/app/(marketing)/open/page.tsx:234
msgid "Merged PRs"
msgstr "PRs fusionados"
@ -345,8 +345,8 @@ msgstr "No se requiere tarjeta de crédito"
msgid "None of these work for you? Try self-hosting!"
msgstr "¿Ninguna de estas opciones funciona para ti? ¡Prueba el autoalojamiento!"
#: apps/marketing/src/app/(marketing)/open/page.tsx:193
#: apps/marketing/src/app/(marketing)/open/page.tsx:251
#: apps/marketing/src/app/(marketing)/open/page.tsx:194
#: apps/marketing/src/app/(marketing)/open/page.tsx:252
msgid "Open Issues"
msgstr "Problemas Abiertos"
@ -354,7 +354,7 @@ msgstr "Problemas Abiertos"
msgid "Open Source or Hosted."
msgstr "Código Abierto o Alojado."
#: apps/marketing/src/app/(marketing)/open/page.tsx:160
#: apps/marketing/src/app/(marketing)/open/page.tsx:161
#: apps/marketing/src/components/(marketing)/footer.tsx:37
#: apps/marketing/src/components/(marketing)/header.tsx:64
#: apps/marketing/src/components/(marketing)/mobile-navigation.tsx:40
@ -466,7 +466,7 @@ msgstr "Inteligente."
msgid "Star on GitHub"
msgstr "Estrella en GitHub"
#: apps/marketing/src/app/(marketing)/open/page.tsx:225
#: apps/marketing/src/app/(marketing)/open/page.tsx:226
msgid "Stars"
msgstr "Estrellas"
@ -501,7 +501,7 @@ msgstr "Tienda de Plantillas (Pronto)."
msgid "That's awesome. You can take a look at the current <0>Issues</0> and join our <1>Discord Community</1> to keep up to date, on what the current priorities are. In any case, we are an open community and welcome all input, technical and non-technical ❤️"
msgstr "Eso es increíble. Puedes echar un vistazo a los <0>Problemas</0> actuales y unirte a nuestra <1>Comunidad de Discord</1> para mantenerte al día sobre cuáles son las prioridades actuales. En cualquier caso, somos una comunidad abierta y agradecemos todas las aportaciones, técnicas y no técnicas ❤️"
#: apps/marketing/src/app/(marketing)/open/page.tsx:292
#: apps/marketing/src/app/(marketing)/open/page.tsx:293
msgid "This page is evolving as we learn what makes a great signing company. We'll update it when we have more to share."
msgstr "Esta página está evolucionando a medida que aprendemos lo que hace una gran empresa de firmas. La actualizaremos cuando tengamos más para compartir."
@ -514,8 +514,8 @@ msgstr "Título"
msgid "Total Completed Documents"
msgstr "Total de Documentos Completados"
#: apps/marketing/src/app/(marketing)/open/page.tsx:266
#: apps/marketing/src/app/(marketing)/open/page.tsx:267
#: apps/marketing/src/app/(marketing)/open/page.tsx:268
msgid "Total Customers"
msgstr "Total de Clientes"
@ -602,4 +602,3 @@ msgstr "Puedes autoalojar Documenso de forma gratuita o usar nuestra versión al
#: apps/marketing/src/components/(marketing)/carousel.tsx:272
msgid "Your browser does not support the video tag."
msgstr "Tu navegador no soporta la etiqueta de video."

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: fr\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-11-05 02:04\n"
"PO-Revision-Date: 2024-11-05 09:34\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
@ -96,6 +96,90 @@ msgstr "{memberEmail} a rejoint l'équipe suivante"
msgid "{memberEmail} left the following team"
msgstr "{memberEmail} a quitté l'équipe suivante"
#: packages/lib/utils/document-audit-logs.ts:263
msgid "{prefix} added a field"
msgstr "{prefix} a ajouté un champ"
#: packages/lib/utils/document-audit-logs.ts:275
msgid "{prefix} added a recipient"
msgstr "{prefix} a ajouté un destinataire"
#: packages/lib/utils/document-audit-logs.ts:287
msgid "{prefix} created the document"
msgstr "{prefix} a créé le document"
#: packages/lib/utils/document-audit-logs.ts:291
msgid "{prefix} deleted the document"
msgstr "{prefix} a supprimé le document"
#: packages/lib/utils/document-audit-logs.ts:335
msgid "{prefix} moved the document to team"
msgstr "{prefix} a déplacé le document vers l'équipe"
#: packages/lib/utils/document-audit-logs.ts:319
msgid "{prefix} opened the document"
msgstr "{prefix} a ouvert le document"
#: packages/lib/utils/document-audit-logs.ts:267
msgid "{prefix} removed a field"
msgstr "{prefix} a supprimé un champ"
#: packages/lib/utils/document-audit-logs.ts:279
msgid "{prefix} removed a recipient"
msgstr "{prefix} a supprimé un destinataire"
#: packages/lib/utils/document-audit-logs.ts:355
msgid "{prefix} resent an email to {0}"
msgstr "{prefix} a renvoyé un e-mail à {0}"
#: packages/lib/utils/document-audit-logs.ts:356
msgid "{prefix} sent an email to {0}"
msgstr "{prefix} a envoyé un email à {0}"
#: packages/lib/utils/document-audit-logs.ts:331
msgid "{prefix} sent the document"
msgstr "{prefix} a envoyé le document"
#: packages/lib/utils/document-audit-logs.ts:295
msgid "{prefix} signed a field"
msgstr "{prefix} a signé un champ"
#: packages/lib/utils/document-audit-logs.ts:299
msgid "{prefix} unsigned a field"
msgstr "{prefix} n'a pas signé un champ"
#: packages/lib/utils/document-audit-logs.ts:271
msgid "{prefix} updated a field"
msgstr "{prefix} a mis à jour un champ"
#: packages/lib/utils/document-audit-logs.ts:283
msgid "{prefix} updated a recipient"
msgstr "{prefix} a mis à jour un destinataire"
#: packages/lib/utils/document-audit-logs.ts:315
msgid "{prefix} updated the document"
msgstr "{prefix} a mis à jour le document"
#: packages/lib/utils/document-audit-logs.ts:307
msgid "{prefix} updated the document access auth requirements"
msgstr "{prefix} a mis à jour les exigences d'authentification d'accès au document"
#: packages/lib/utils/document-audit-logs.ts:327
msgid "{prefix} updated the document external ID"
msgstr "{prefix} a mis à jour l'ID externe du document"
#: packages/lib/utils/document-audit-logs.ts:311
msgid "{prefix} updated the document signing auth requirements"
msgstr "{prefix} a mis à jour les exigences d'authentification pour la signature du document"
#: packages/lib/utils/document-audit-logs.ts:323
msgid "{prefix} updated the document title"
msgstr "{prefix} a mis à jour le titre du document"
#: packages/lib/utils/document-audit-logs.ts:303
msgid "{prefix} updated the document visibility"
msgstr "{prefix} a mis à jour la visibilité du document"
#: packages/email/templates/document-created-from-direct-template.tsx:55
msgid "{recipientName} {action} a document by using one of your direct links"
msgstr "{recipientName} {action} un document en utilisant l'un de vos liens directs"
@ -104,6 +188,26 @@ msgstr "{recipientName} {action} un document en utilisant l'un de vos liens dire
msgid "{teamName} ownership transfer request"
msgstr "Demande de transfert de propriété de {teamName}"
#: packages/lib/utils/document-audit-logs.ts:343
msgid "{userName} approved the document"
msgstr "{userName} a approuvé le document"
#: packages/lib/utils/document-audit-logs.ts:344
msgid "{userName} CC'd the document"
msgstr "{userName} a mis en copie le document"
#: packages/lib/utils/document-audit-logs.ts:345
msgid "{userName} completed their task"
msgstr "{userName} a complété sa tâche"
#: packages/lib/utils/document-audit-logs.ts:341
msgid "{userName} signed the document"
msgstr "{userName} a signé le document"
#: packages/lib/utils/document-audit-logs.ts:342
msgid "{userName} viewed the document"
msgstr "{userName} a consulté le document"
#: packages/ui/primitives/data-table-pagination.tsx:41
msgid "{visibleRows, plural, one {Showing # result.} other {Showing # results.}}"
msgstr "{visibleRows, plural, one {Affichage de # résultat.} other {Affichage de # résultats.}}"
@ -150,10 +254,34 @@ msgstr "<0>Exiger une clé d'accès</0> - Le destinataire doit avoir un compte e
msgid "A document was created by your direct template that requires you to {recipientActionVerb} it."
msgstr "Un document a été créé par votre modèle direct qui nécessite que vous {recipientActionVerb} celui-ci."
#: packages/lib/utils/document-audit-logs.ts:262
msgid "A field was added"
msgstr "Un champ a été ajouté"
#: packages/lib/utils/document-audit-logs.ts:266
msgid "A field was removed"
msgstr "Un champ a été supprimé"
#: packages/lib/utils/document-audit-logs.ts:270
msgid "A field was updated"
msgstr "Un champ a été mis à jour"
#: packages/lib/jobs/definitions/emails/send-team-member-joined-email.ts:90
msgid "A new member has joined your team"
msgstr "Un nouveau membre a rejoint votre équipe"
#: packages/lib/utils/document-audit-logs.ts:274
msgid "A recipient was added"
msgstr "Un destinataire a été ajouté"
#: packages/lib/utils/document-audit-logs.ts:278
msgid "A recipient was removed"
msgstr "Un destinataire a été supprimé"
#: packages/lib/utils/document-audit-logs.ts:282
msgid "A recipient was updated"
msgstr "Un destinataire a été mis à jour"
#: packages/lib/server-only/team/create-team-email-verification.ts:142
msgid "A request to use your email has been initiated by {teamName} on Documenso"
msgstr "Une demande d'utilisation de votre email a été initiée par {teamName} sur Documenso"
@ -368,6 +496,7 @@ msgstr "Fermer"
#: packages/email/template-components/template-document-completed.tsx:35
#: packages/email/template-components/template-document-self-signed.tsx:36
#: packages/lib/constants/document.ts:10
msgid "Completed"
msgstr "Terminé"
@ -450,10 +579,24 @@ msgstr "Receveur de lien direct"
msgid "Document access"
msgstr "Accès au document"
#: packages/lib/utils/document-audit-logs.ts:306
msgid "Document access auth updated"
msgstr "L'authentification d'accès au document a été mise à jour"
#: packages/lib/server-only/document/delete-document.ts:213
#: packages/lib/server-only/document/super-delete-document.ts:75
msgid "Document Cancelled"
msgstr "Document Annulé"
#: packages/lib/utils/document-audit-logs.ts:359
#: packages/lib/utils/document-audit-logs.ts:360
msgid "Document completed"
msgstr "Document terminé"
#: packages/lib/utils/document-audit-logs.ts:286
msgid "Document created"
msgstr "Document créé"
#: packages/email/templates/document-created-from-direct-template.tsx:30
#: packages/lib/server-only/template/create-document-from-direct-template.ts:554
msgid "Document created from direct template"
@ -463,15 +606,55 @@ msgstr "Document créé à partir d'un modèle direct"
msgid "Document Creation"
msgstr "Création de document"
#: packages/lib/utils/document-audit-logs.ts:290
msgid "Document deleted"
msgstr "Document supprimé"
#: packages/lib/server-only/document/send-delete-email.ts:58
msgid "Document Deleted!"
msgstr "Document Supprimé !"
#: packages/lib/utils/document-audit-logs.ts:326
msgid "Document external ID updated"
msgstr "ID externe du document mis à jour"
#: packages/lib/utils/document-audit-logs.ts:334
msgid "Document moved to team"
msgstr "Document déplacé vers l'équipe"
#: packages/lib/utils/document-audit-logs.ts:318
msgid "Document opened"
msgstr "Document ouvert"
#: packages/lib/utils/document-audit-logs.ts:330
msgid "Document sent"
msgstr "Document envoyé"
#: packages/lib/utils/document-audit-logs.ts:310
msgid "Document signing auth updated"
msgstr "Authentification de signature de document mise à jour"
#: packages/lib/utils/document-audit-logs.ts:322
msgid "Document title updated"
msgstr "Titre du document mis à jour"
#: packages/lib/utils/document-audit-logs.ts:314
msgid "Document updated"
msgstr "Document mis à jour"
#: packages/lib/utils/document-audit-logs.ts:302
msgid "Document visibility updated"
msgstr "Visibilité du document mise à jour"
#: packages/email/template-components/template-document-completed.tsx:64
#: packages/ui/components/document/document-download-button.tsx:68
msgid "Download"
msgstr "Télécharger"
#: packages/lib/constants/document.ts:13
msgid "Draft"
msgstr "Brouillon"
#: packages/ui/primitives/document-dropzone.tsx:162
msgid "Drag & drop your PDF here."
msgstr "Faites glisser et déposez votre PDF ici."
@ -504,6 +687,14 @@ msgstr "L'email est requis"
msgid "Email Options"
msgstr "Options d'email"
#: packages/lib/utils/document-audit-logs.ts:353
msgid "Email resent"
msgstr "Email renvoyé"
#: packages/lib/utils/document-audit-logs.ts:353
msgid "Email sent"
msgstr "Email envoyé"
#: packages/ui/primitives/document-flow/add-fields.tsx:1123
msgid "Empty field"
msgstr "Champ vide"
@ -564,6 +755,14 @@ msgstr "Étiquette du champ"
msgid "Field placeholder"
msgstr "Espace réservé du champ"
#: packages/lib/utils/document-audit-logs.ts:294
msgid "Field signed"
msgstr "Champ signé"
#: packages/lib/utils/document-audit-logs.ts:298
msgid "Field unsigned"
msgstr "Champ non signé"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/date-field.tsx:56
#: packages/ui/primitives/document-flow/field-items-advanced-settings/email-field.tsx:38
#: packages/ui/primitives/document-flow/field-items-advanced-settings/initials-field.tsx:38
@ -774,6 +973,10 @@ msgstr "Réinitialisation du mot de passe réussie"
msgid "Password updated!"
msgstr "Mot de passe mis à jour !"
#: packages/lib/constants/document.ts:16
msgid "Pending"
msgstr "En attente"
#: packages/email/templates/document-pending.tsx:17
msgid "Pending Document"
msgstr "Document En Attente"
@ -841,6 +1044,10 @@ msgstr "Lecture seule"
msgid "Receives copy"
msgstr "Recevoir une copie"
#: packages/lib/utils/document-audit-logs.ts:338
msgid "Recipient"
msgstr "Destinataire"
#: packages/ui/components/recipient/recipient-action-auth-select.tsx:39
#: packages/ui/primitives/document-flow/add-settings.tsx:257
#: packages/ui/primitives/template-flow/add-template-settings.tsx:208
@ -1248,6 +1455,10 @@ msgstr "Nous avons changé votre mot de passe comme demandé. Vous pouvez mainte
msgid "Welcome to Documenso!"
msgstr "Bienvenue sur Documenso !"
#: packages/lib/utils/document-audit-logs.ts:258
msgid "You"
msgstr "Vous"
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx:44
msgid "You are about to send this document to the recipients. Are you sure you want to continue?"
msgstr "Vous êtes sur le point d'envoyer ce document aux destinataires. Êtes-vous sûr de vouloir continuer ?"
@ -1309,4 +1520,3 @@ msgstr "Votre mot de passe a été mis à jour."
#: packages/email/templates/team-delete.tsx:30
msgid "Your team has been deleted"
msgstr "Votre équipe a été supprimée"

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: fr\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-11-05 02:04\n"
"PO-Revision-Date: 2024-11-05 09:34\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
@ -42,7 +42,7 @@ msgstr "Ajouter un document"
msgid "Add More Users for {0}"
msgstr "Ajouter plus d'utilisateurs pour {0}"
#: apps/marketing/src/app/(marketing)/open/page.tsx:164
#: apps/marketing/src/app/(marketing)/open/page.tsx:165
msgid "All our metrics, finances, and learnings are public. We believe in transparency and want to share our journey with you. You can read more about why here: <0>Announcing Open Metrics</0>"
msgstr "Tous nos indicateurs, finances et apprentissages sont publics. Nous croyons en la transparence et souhaitons partager notre parcours avec vous. Vous pouvez en lire plus sur pourquoi ici : <0>Annonce de Open Metrics</0>"
@ -90,7 +90,7 @@ msgstr "Changelog"
msgid "Choose a template from the community app store. Or submit your own template for others to use."
msgstr "Choisissez un modèle dans la boutique d'applications communautaires. Ou soumettez votre propre modèle pour que d'autres puissent l'utiliser."
#: apps/marketing/src/app/(marketing)/open/page.tsx:218
#: apps/marketing/src/app/(marketing)/open/page.tsx:219
msgid "Community"
msgstr "Communauté"
@ -193,7 +193,7 @@ msgstr "Rapide."
msgid "Faster, smarter and more beautiful."
msgstr "Plus rapide, plus intelligent et plus beau."
#: apps/marketing/src/app/(marketing)/open/page.tsx:209
#: apps/marketing/src/app/(marketing)/open/page.tsx:210
msgid "Finances"
msgstr "Finances"
@ -246,15 +246,15 @@ msgstr "Commencez aujourd'hui."
msgid "Get the latest news from Documenso, including product updates, team announcements and more!"
msgstr "Obtenez les dernières nouvelles de Documenso, y compris les mises à jour de produits, les annonces d'équipe et plus encore !"
#: apps/marketing/src/app/(marketing)/open/page.tsx:232
#: apps/marketing/src/app/(marketing)/open/page.tsx:233
msgid "GitHub: Total Merged PRs"
msgstr "GitHub : Total des PRs fusionnées"
#: apps/marketing/src/app/(marketing)/open/page.tsx:250
#: apps/marketing/src/app/(marketing)/open/page.tsx:251
msgid "GitHub: Total Open Issues"
msgstr "GitHub : Total des problèmes ouverts"
#: apps/marketing/src/app/(marketing)/open/page.tsx:224
#: apps/marketing/src/app/(marketing)/open/page.tsx:225
msgid "GitHub: Total Stars"
msgstr "GitHub : Nombre total d'étoiles"
@ -262,7 +262,7 @@ msgstr "GitHub : Nombre total d'étoiles"
msgid "Global Salary Bands"
msgstr "Bandes de salaire globales"
#: apps/marketing/src/app/(marketing)/open/page.tsx:260
#: apps/marketing/src/app/(marketing)/open/page.tsx:261
msgid "Growth"
msgstr "Croissance"
@ -286,7 +286,7 @@ msgstr "Paiements intégrés avec Stripe afin que vous n'ayez pas à vous soucie
msgid "Integrates with all your favourite tools."
msgstr "S'intègre à tous vos outils préférés."
#: apps/marketing/src/app/(marketing)/open/page.tsx:288
#: apps/marketing/src/app/(marketing)/open/page.tsx:289
msgid "Is there more?"
msgstr "Y a-t-il plus ?"
@ -310,11 +310,11 @@ msgstr "Emplacement"
msgid "Make it your own through advanced customization and adjustability."
msgstr "Faites-en votre propre grâce à une personnalisation avancée et un ajustement."
#: apps/marketing/src/app/(marketing)/open/page.tsx:198
#: apps/marketing/src/app/(marketing)/open/page.tsx:199
msgid "Merged PR's"
msgstr "PRs fusionnées"
#: apps/marketing/src/app/(marketing)/open/page.tsx:233
#: apps/marketing/src/app/(marketing)/open/page.tsx:234
msgid "Merged PRs"
msgstr "PRs fusionnées"
@ -345,8 +345,8 @@ msgstr "Aucune carte de crédit requise"
msgid "None of these work for you? Try self-hosting!"
msgstr "Aucune de ces options ne fonctionne pour vous ? Essayez l'hébergement autonome !"
#: apps/marketing/src/app/(marketing)/open/page.tsx:193
#: apps/marketing/src/app/(marketing)/open/page.tsx:251
#: apps/marketing/src/app/(marketing)/open/page.tsx:194
#: apps/marketing/src/app/(marketing)/open/page.tsx:252
msgid "Open Issues"
msgstr "Problèmes ouverts"
@ -354,7 +354,7 @@ msgstr "Problèmes ouverts"
msgid "Open Source or Hosted."
msgstr "Open Source ou hébergé."
#: apps/marketing/src/app/(marketing)/open/page.tsx:160
#: apps/marketing/src/app/(marketing)/open/page.tsx:161
#: apps/marketing/src/components/(marketing)/footer.tsx:37
#: apps/marketing/src/components/(marketing)/header.tsx:64
#: apps/marketing/src/components/(marketing)/mobile-navigation.tsx:40
@ -466,7 +466,7 @@ msgstr "Intelligent."
msgid "Star on GitHub"
msgstr "Étoile sur GitHub"
#: apps/marketing/src/app/(marketing)/open/page.tsx:225
#: apps/marketing/src/app/(marketing)/open/page.tsx:226
msgid "Stars"
msgstr "Étoiles"
@ -501,7 +501,7 @@ msgstr "Boutique de modèles (Bientôt)."
msgid "That's awesome. You can take a look at the current <0>Issues</0> and join our <1>Discord Community</1> to keep up to date, on what the current priorities are. In any case, we are an open community and welcome all input, technical and non-technical ❤️"
msgstr "C'est génial. Vous pouvez consulter les <0>Problèmes</0> actuels et rejoindre notre <1>Communauté Discord</1> pour rester à jour sur ce qui est actuellement prioritaire. Dans tous les cas, nous sommes une communauté ouverte et accueillons toutes les contributions, techniques et non techniques ❤️"
#: apps/marketing/src/app/(marketing)/open/page.tsx:292
#: apps/marketing/src/app/(marketing)/open/page.tsx:293
msgid "This page is evolving as we learn what makes a great signing company. We'll update it when we have more to share."
msgstr "Cette page évolue à mesure que nous apprenons ce qui fait une grande entreprise de signature. Nous la mettrons à jour lorsque nous aurons plus à partager."
@ -514,8 +514,8 @@ msgstr "Titre"
msgid "Total Completed Documents"
msgstr "Documents totalisés complétés"
#: apps/marketing/src/app/(marketing)/open/page.tsx:266
#: apps/marketing/src/app/(marketing)/open/page.tsx:267
#: apps/marketing/src/app/(marketing)/open/page.tsx:268
msgid "Total Customers"
msgstr "Total des clients"
@ -602,4 +602,3 @@ msgstr "Vous pouvez auto-héberger Documenso gratuitement ou utiliser notre vers
#: apps/marketing/src/components/(marketing)/carousel.tsx:272
msgid "Your browser does not support the video tag."
msgstr "Votre navigateur ne prend pas en charge la balise vidéo."

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,5 +1,9 @@
import { type Field, type Recipient, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
export const formatSigningLink = (token: string) => `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${token}`;
/**
* Whether a recipient can be modified by the document owner.
*/

View File

@ -11,6 +11,7 @@ import { createDocument } from '@documenso/lib/server-only/document/create-docum
import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
import { duplicateDocumentById } from '@documenso/lib/server-only/document/duplicate-document-by-id';
import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-document-audit-logs';
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
@ -31,6 +32,7 @@ import {
ZDownloadAuditLogsMutationSchema,
ZDownloadCertificateMutationSchema,
ZFindDocumentAuditLogsQuerySchema,
ZFindDocumentsQuerySchema,
ZGetDocumentByIdQuerySchema,
ZGetDocumentByTokenQuerySchema,
ZGetDocumentWithDetailsByIdQuerySchema,
@ -190,6 +192,37 @@ export const documentRouter = router({
}
}),
findDocuments: authenticatedProcedure
.input(ZFindDocumentsQuerySchema)
.query(async ({ input, ctx }) => {
const { user } = ctx;
const { search, teamId, templateId, page, perPage, orderBy, source, status } = input;
try {
const documents = await findDocuments({
userId: user.id,
teamId,
templateId,
search,
source,
status,
page,
perPage,
orderBy,
});
return documents;
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We are unable to search for documents. Please try again later.',
});
}
}),
findDocumentAuditLogs: authenticatedProcedure
.input(ZFindDocumentAuditLogsQuerySchema)
.query(async ({ input, ctx }) => {

View File

@ -7,7 +7,30 @@ import {
} from '@documenso/lib/types/document-auth';
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
import { DocumentSigningOrder, FieldType, RecipientRole } from '@documenso/prisma/client';
import {
DocumentSigningOrder,
DocumentSource,
DocumentStatus,
FieldType,
RecipientRole,
} from '@documenso/prisma/client';
export const ZFindDocumentsQuerySchema = ZBaseTableSearchParamsSchema.extend({
teamId: z.number().min(1).optional(),
templateId: z.number().min(1).optional(),
search: z
.string()
.optional()
.catch(() => undefined),
source: z.nativeEnum(DocumentSource).optional(),
status: z.nativeEnum(DocumentStatus).optional(),
orderBy: z
.object({
column: z.enum(['createdAt']),
direction: z.enum(['asc', 'desc']),
})
.optional(),
}).omit({ query: true });
export const ZFindDocumentAuditLogsQuerySchema = ZBaseTableSearchParamsSchema.extend({
documentId: z.number().min(1),

View File

@ -0,0 +1,82 @@
'use client';
import { useState } from 'react';
import { motion } from 'framer-motion';
import { AnimatePresence } from 'framer-motion';
import { CheckSquareIcon, CopyIcon } from 'lucide-react';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
import { Button } from '@documenso/ui/primitives/button';
import { cn } from '../../lib/utils';
export type CopyTextButtonProps = {
value: string;
badgeContentUncopied?: React.ReactNode;
badgeContentCopied?: React.ReactNode;
onCopySuccess?: () => void;
};
export const CopyTextButton = ({
value,
onCopySuccess,
badgeContentUncopied,
badgeContentCopied,
}: CopyTextButtonProps) => {
const [, copy] = useCopyToClipboard();
const [copiedTimeout, setCopiedTimeout] = useState<NodeJS.Timeout | null>(null);
const onCopy = async () => {
await copy(value).then(() => onCopySuccess?.());
if (copiedTimeout) {
clearTimeout(copiedTimeout);
}
setCopiedTimeout(
setTimeout(() => {
setCopiedTimeout(null);
}, 2000),
);
};
return (
<Button
type="button"
variant="none"
className="ml-2 h-7 rounded border bg-neutral-50 px-0.5 font-normal dark:border dark:border-neutral-500 dark:bg-neutral-600"
onClick={async () => onCopy()}
>
<AnimatePresence mode="wait" initial={false}>
<motion.div
className="flex flex-row items-center"
key={copiedTimeout ? 'copied' : 'copy'}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, transition: { duration: 0.1 } }}
>
{copiedTimeout ? badgeContentCopied : badgeContentUncopied}
<div
className={cn(
'flex h-6 w-6 items-center justify-center rounded transition-all hover:bg-neutral-200 hover:active:bg-neutral-300 dark:hover:bg-neutral-500 dark:hover:active:bg-neutral-400',
{
'ml-1': Boolean(badgeContentCopied || badgeContentUncopied),
},
)}
>
<div className="absolute">
{copiedTimeout ? (
<CheckSquareIcon className="h-3.5 w-3.5" />
) : (
<CopyIcon className="h-3.5 w-3.5" />
)}
</div>
</div>
</motion.div>
</AnimatePresence>
</Button>
);
};

View File

@ -105,6 +105,7 @@
"NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS",
"NEXT_PRIVATE_SMTP_FROM_NAME",
"NEXT_PRIVATE_SMTP_FROM_ADDRESS",
"NEXT_PRIVATE_SMTP_SERVICE",
"NEXT_PRIVATE_STRIPE_API_KEY",
"NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET",
"NEXT_PRIVATE_GITHUB_TOKEN",