From 6a41a37bd447b55f44b18fea7f2fd87a70676adc Mon Sep 17 00:00:00 2001 From: Ephraim Duncan <55143799+ephraimduncan@users.noreply.github.com> Date: Fri, 25 Apr 2025 12:44:03 +0000 Subject: [PATCH 01/12] feat: download original documents (#1742) ## Preview ![CleanShot 2025-04-10 at 14 26 11@2x](https://github.com/user-attachments/assets/d4984d85-ab40-4d38-8d5c-a1085bde21a2) --- .../document/document-page-view-dropdown.tsx | 39 +++++++++++++++++-- .../documents-table-action-dropdown.tsx | 31 +++++++++++++++ packages/lib/client-only/download-pdf.ts | 22 +++++++++-- packages/ui/styles/theme.css | 4 +- 4 files changed, 88 insertions(+), 8 deletions(-) diff --git a/apps/remix/app/components/general/document/document-page-view-dropdown.tsx b/apps/remix/app/components/general/document/document-page-view-dropdown.tsx index 21f4ce481..bb2164f52 100644 --- a/apps/remix/app/components/general/document/document-page-view-dropdown.tsx +++ b/apps/remix/app/components/general/document/document-page-view-dropdown.tsx @@ -3,8 +3,8 @@ import { useState } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import { DocumentStatus } from '@prisma/client'; import type { Document, Recipient, Team, User } from '@prisma/client'; +import { DocumentStatus } from '@prisma/client'; import { Copy, Download, @@ -15,8 +15,7 @@ import { Share, Trash2, } from 'lucide-react'; -import { Link } from 'react-router'; -import { useNavigate } from 'react-router'; +import { Link, useNavigate } from 'react-router'; import { downloadPDF } from '@documenso/lib/client-only/download-pdf'; import { useSession } from '@documenso/lib/client-only/providers/session'; @@ -99,6 +98,35 @@ export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownP } }; + const onDownloadOriginalClick = async () => { + try { + const documentWithData = await trpcClient.document.getDocumentById.query( + { + documentId: document.id, + }, + { + context: { + teamId: team?.id?.toString(), + }, + }, + ); + + const documentData = documentWithData?.documentData; + + if (!documentData) { + return; + } + + await downloadPDF({ documentData, fileName: document.title, version: 'original' }); + } catch (err) { + toast({ + title: _(msg`Something went wrong`), + description: _(msg`An error occurred while downloading your document.`), + variant: 'destructive', + }); + } + }; + const nonSignedRecipients = document.recipients.filter((item) => item.signingStatus !== 'SIGNED'); return ( @@ -128,6 +156,11 @@ export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownP )} + + + Download Original + + diff --git a/apps/remix/app/components/tables/documents-table-action-dropdown.tsx b/apps/remix/app/components/tables/documents-table-action-dropdown.tsx index ea0f11fc1..21e203a50 100644 --- a/apps/remix/app/components/tables/documents-table-action-dropdown.tsx +++ b/apps/remix/app/components/tables/documents-table-action-dropdown.tsx @@ -100,6 +100,32 @@ export const DocumentsTableActionDropdown = ({ row }: DocumentsTableActionDropdo } }; + const onDownloadOriginalClick = async () => { + try { + const document = !recipient + ? await trpcClient.document.getDocumentById.query({ + documentId: row.id, + }) + : await trpcClient.document.getDocumentByToken.query({ + token: recipient.token, + }); + + const documentData = document?.documentData; + + if (!documentData) { + return; + } + + await downloadPDF({ documentData, fileName: row.title, version: 'original' }); + } catch (err) { + toast({ + title: _(msg`Something went wrong`), + description: _(msg`An error occurred while downloading your document.`), + variant: 'destructive', + }); + } + }; + const nonSignedRecipients = row.recipients.filter((item) => item.signingStatus !== 'SIGNED'); return ( @@ -152,6 +178,11 @@ export const DocumentsTableActionDropdown = ({ row }: DocumentsTableActionDropdo Download + + + Download Original + + setDuplicateDialogOpen(true)}> Duplicate diff --git a/packages/lib/client-only/download-pdf.ts b/packages/lib/client-only/download-pdf.ts index 65c8ca4f6..f6db9b629 100644 --- a/packages/lib/client-only/download-pdf.ts +++ b/packages/lib/client-only/download-pdf.ts @@ -3,22 +3,38 @@ import type { DocumentData } from '@prisma/client'; import { getFile } from '../universal/upload/get-file'; import { downloadFile } from './download-file'; +type DocumentVersion = 'original' | 'signed'; + type DownloadPDFProps = { documentData: DocumentData; fileName?: string; + /** + * Specifies which version of the document to download. + * 'signed': Downloads the signed version (default). + * 'original': Downloads the original version. + */ + version?: DocumentVersion; }; -export const downloadPDF = async ({ documentData, fileName }: DownloadPDFProps) => { - const bytes = await getFile(documentData); +export const downloadPDF = async ({ + documentData, + fileName, + version = 'signed', +}: DownloadPDFProps) => { + const bytes = await getFile({ + type: documentData.type, + data: version === 'signed' ? documentData.data : documentData.initialData, + }); const blob = new Blob([bytes], { type: 'application/pdf', }); const baseTitle = (fileName ?? 'document').replace(/\.pdf$/, ''); + const suffix = version === 'signed' ? '_signed.pdf' : '.pdf'; downloadFile({ - filename: `${baseTitle}_signed.pdf`, + filename: `${baseTitle}${suffix}`, data: blob, }); }; diff --git a/packages/ui/styles/theme.css b/packages/ui/styles/theme.css index facb8be4c..f82f2b626 100644 --- a/packages/ui/styles/theme.css +++ b/packages/ui/styles/theme.css @@ -230,7 +230,7 @@ scrollbar-gutter: stable; } -.custom-scrollbar::-webkit-scrollbar-track { +/* .custom-scrollbar::-webkit-scrollbar-track { border-radius: 10px; } @@ -242,7 +242,7 @@ .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: rgb(100 116 139 / 0.5); -} +} */ /* Custom Swagger Dark Theme */ .swagger-dark-theme .swagger-ui { From bdb0b0ea88bee2779180c8d98087b325c5dc2cf8 Mon Sep 17 00:00:00 2001 From: Ephraim Duncan <55143799+ephraimduncan@users.noreply.github.com> Date: Mon, 28 Apr 2025 01:30:09 +0000 Subject: [PATCH 02/12] feat: certificate qrcode (#1755) Adds document access tokens and QR code functionality to enable secure document sharing via URLs. It includes a new document access page that allows viewing and downloading documents through tokenized links. --- .../dialogs/document-resend-dialog.tsx | 8 +- .../document/document-certificate-qr-view.tsx | 106 ++++++++++++++++++ .../share-document-download-button.tsx | 53 +++++++++ .../tables/documents-table-action-button.tsx | 8 +- .../documents-table-action-dropdown.tsx | 8 +- .../tables/documents-table-title.tsx | 8 +- .../_internal+/[__htmltopdf]+/certificate.tsx | 15 ++- apps/remix/app/routes/_share+/_layout.tsx | 11 ++ .../share.$slug.opengraph.tsx | 5 + .../share.$slug.tsx | 40 ++++++- .../internal/seal-document.handler.ts | 12 ++ .../document/create-document-v2.ts | 3 +- .../server-only/document/create-document.ts | 2 + .../document/duplicate-document-by-id.ts | 2 + .../document/get-document-by-access-token.ts | 38 +++++++ .../create-document-from-direct-template.ts | 3 +- .../create-document-from-template-legacy.ts | 3 +- .../template/create-document-from-template.ts | 3 +- packages/lib/types/document.ts | 4 + packages/lib/universal/id.ts | 6 + packages/prisma/index.ts | 34 ++++++ .../migration.sql | 2 + packages/prisma/schema.prisma | 1 + .../get-document-internal-url-for-qr-code.ts | 61 ++++++++++ ...document-internal-url-for-qr-code.types.ts | 15 +++ .../trpc/server/share-link-router/router.ts | 3 + 26 files changed, 423 insertions(+), 31 deletions(-) create mode 100644 apps/remix/app/components/general/document/document-certificate-qr-view.tsx create mode 100644 apps/remix/app/components/general/share-document-download-button.tsx create mode 100644 apps/remix/app/routes/_share+/_layout.tsx rename apps/remix/app/routes/{_unauthenticated+ => _share+}/share.$slug.opengraph.tsx (97%) rename apps/remix/app/routes/{_unauthenticated+ => _share+}/share.$slug.tsx (57%) create mode 100644 packages/lib/server-only/document/get-document-by-access-token.ts create mode 100644 packages/prisma/migrations/20250425015123_add_qr_token_to_document/migration.sql create mode 100644 packages/trpc/server/share-link-router/get-document-internal-url-for-qr-code.ts create mode 100644 packages/trpc/server/share-link-router/get-document-internal-url-for-qr-code.types.ts diff --git a/apps/remix/app/components/dialogs/document-resend-dialog.tsx b/apps/remix/app/components/dialogs/document-resend-dialog.tsx index bcd1d61a3..0847eca0f 100644 --- a/apps/remix/app/components/dialogs/document-resend-dialog.tsx +++ b/apps/remix/app/components/dialogs/document-resend-dialog.tsx @@ -4,14 +4,14 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import type { Team } from '@prisma/client'; -import { type Document, type Recipient, SigningStatus } from '@prisma/client'; +import { type Recipient, SigningStatus } from '@prisma/client'; import { History } from 'lucide-react'; import { useForm } from 'react-hook-form'; import * as z from 'zod'; import { useSession } from '@documenso/lib/client-only/providers/session'; import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; +import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document'; import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter'; import { trpc as trpcReact } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; @@ -43,9 +43,7 @@ import { StackAvatar } from '../general/stack-avatar'; const FORM_ID = 'resend-email'; export type DocumentResendDialogProps = { - document: Document & { - team: Pick | null; - }; + document: TDocumentRow; recipients: Recipient[]; }; diff --git a/apps/remix/app/components/general/document/document-certificate-qr-view.tsx b/apps/remix/app/components/general/document/document-certificate-qr-view.tsx new file mode 100644 index 000000000..20d13a007 --- /dev/null +++ b/apps/remix/app/components/general/document/document-certificate-qr-view.tsx @@ -0,0 +1,106 @@ +import { useEffect, useState } from 'react'; + +import { Trans } from '@lingui/react/macro'; +import type { DocumentData } from '@prisma/client'; +import { DateTime } from 'luxon'; + +import { trpc } from '@documenso/trpc/react'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@documenso/ui/primitives/dialog'; +import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer'; + +import { ShareDocumentDownloadButton } from '../share-document-download-button'; + +export type DocumentCertificateQRViewProps = { + documentId: number; + title: string; + documentData: DocumentData; + password?: string | null; + recipientCount?: number; + completedDate?: Date; +}; + +export const DocumentCertificateQRView = ({ + documentId, + title, + documentData, + password, + recipientCount = 0, + completedDate, +}: DocumentCertificateQRViewProps) => { + const { data: documentUrl } = trpc.shareLink.getDocumentInternalUrlForQRCode.useQuery({ + documentId, + }); + + const [isDialogOpen, setIsDialogOpen] = useState(() => !!documentUrl); + + const formattedDate = completedDate + ? DateTime.fromJSDate(completedDate).toLocaleString(DateTime.DATETIME_MED) + : ''; + + useEffect(() => { + if (documentUrl) { + setIsDialogOpen(true); + } + }, [documentUrl]); + + return ( +
+ {/* Dialog for internal document link */} + {documentUrl && ( + + + + + Document found in your account + + + + + This document is available in your Documenso account. You can view more details, + recipients, and audit logs there. + + + + + + + + + + )} + +
+
+

{title}

+
+

+ {recipientCount} recipients +

+ +

+ Completed on {formattedDate} +

+
+
+ + +
+ +
+ +
+
+ ); +}; diff --git a/apps/remix/app/components/general/share-document-download-button.tsx b/apps/remix/app/components/general/share-document-download-button.tsx new file mode 100644 index 000000000..c79ac4f51 --- /dev/null +++ b/apps/remix/app/components/general/share-document-download-button.tsx @@ -0,0 +1,53 @@ +import { useState } from 'react'; + +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { Download } from 'lucide-react'; + +import { downloadPDF } from '@documenso/lib/client-only/download-pdf'; +import type { DocumentData } from '@documenso/prisma/client'; +import { Button } from '@documenso/ui/primitives/button'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +export type ShareDocumentDownloadButtonProps = { + title: string; + documentData: DocumentData; +}; + +export const ShareDocumentDownloadButton = ({ + title, + documentData, +}: ShareDocumentDownloadButtonProps) => { + const { _ } = useLingui(); + const { toast } = useToast(); + + const [isDownloading, setIsDownloading] = useState(false); + + const onDownloadClick = async () => { + try { + setIsDownloading(true); + + await new Promise((resolve) => { + setTimeout(resolve, 4000); + }); + + await downloadPDF({ documentData, fileName: title }); + } catch (err) { + toast({ + title: _(msg`Something went wrong`), + description: _(msg`An error occurred while downloading your document.`), + variant: 'destructive', + }); + } finally { + setIsDownloading(false); + } + }; + + return ( + + ); +}; diff --git a/apps/remix/app/components/tables/documents-table-action-button.tsx b/apps/remix/app/components/tables/documents-table-action-button.tsx index 97230c359..20331de97 100644 --- a/apps/remix/app/components/tables/documents-table-action-button.tsx +++ b/apps/remix/app/components/tables/documents-table-action-button.tsx @@ -1,7 +1,6 @@ import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import type { Document, Recipient, Team, User } from '@prisma/client'; import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client'; import { CheckCircle, Download, Edit, EyeIcon, Pencil } from 'lucide-react'; import { Link } from 'react-router'; @@ -9,6 +8,7 @@ import { match } from 'ts-pattern'; import { downloadPDF } from '@documenso/lib/client-only/download-pdf'; import { useSession } from '@documenso/lib/client-only/providers/session'; +import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document'; import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { trpc as trpcClient } from '@documenso/trpc/client'; @@ -18,11 +18,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast'; import { useOptionalCurrentTeam } from '~/providers/team'; export type DocumentsTableActionButtonProps = { - row: Document & { - user: Pick; - recipients: Recipient[]; - team: Pick | null; - }; + row: TDocumentRow; }; export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonProps) => { diff --git a/apps/remix/app/components/tables/documents-table-action-dropdown.tsx b/apps/remix/app/components/tables/documents-table-action-dropdown.tsx index 21e203a50..73bf010bc 100644 --- a/apps/remix/app/components/tables/documents-table-action-dropdown.tsx +++ b/apps/remix/app/components/tables/documents-table-action-dropdown.tsx @@ -3,7 +3,6 @@ import { useState } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import type { Document, Recipient, Team, User } from '@prisma/client'; import { DocumentStatus, RecipientRole } from '@prisma/client'; import { CheckCircle, @@ -22,6 +21,7 @@ import { Link } from 'react-router'; import { downloadPDF } from '@documenso/lib/client-only/download-pdf'; import { useSession } from '@documenso/lib/client-only/providers/session'; +import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document'; import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { trpc as trpcClient } from '@documenso/trpc/client'; @@ -43,11 +43,7 @@ import { DocumentRecipientLinkCopyDialog } from '~/components/general/document/d import { useOptionalCurrentTeam } from '~/providers/team'; export type DocumentsTableActionDropdownProps = { - row: Document & { - user: Pick; - recipients: Recipient[]; - team: Pick | null; - }; + row: TDocumentRow; }; export const DocumentsTableActionDropdown = ({ row }: DocumentsTableActionDropdownProps) => { diff --git a/apps/remix/app/components/tables/documents-table-title.tsx b/apps/remix/app/components/tables/documents-table-title.tsx index f31a8dd94..7946b4ea9 100644 --- a/apps/remix/app/components/tables/documents-table-title.tsx +++ b/apps/remix/app/components/tables/documents-table-title.tsx @@ -1,16 +1,12 @@ -import type { Document, Recipient, Team, User } from '@prisma/client'; import { Link } from 'react-router'; import { match } from 'ts-pattern'; import { useSession } from '@documenso/lib/client-only/providers/session'; +import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; export type DataTableTitleProps = { - row: Document & { - user: Pick; - team: Pick | null; - recipients: Recipient[]; - }; + row: TDocumentRow; teamUrl?: string; }; diff --git a/apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx b/apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx index acf11e935..951e6b582 100644 --- a/apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx +++ b/apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx @@ -5,8 +5,10 @@ import { DateTime } from 'luxon'; import { redirect } from 'react-router'; import { match } from 'ts-pattern'; import { UAParser } from 'ua-parser-js'; +import { renderSVG } from 'uqr'; import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform'; +import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { APP_I18N_OPTIONS, ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n'; import { RECIPIENT_ROLES_DESCRIPTION, @@ -342,7 +344,18 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps) {isPlatformDocument && ( -
+
+
+
+
+

{_(msg`Signing certificate provided by`)}: diff --git a/apps/remix/app/routes/_share+/_layout.tsx b/apps/remix/app/routes/_share+/_layout.tsx new file mode 100644 index 000000000..39b519864 --- /dev/null +++ b/apps/remix/app/routes/_share+/_layout.tsx @@ -0,0 +1,11 @@ +import { Outlet } from 'react-router'; + +export default function Layout() { + return ( +

+
+ +
+
+ ); +} diff --git a/apps/remix/app/routes/_unauthenticated+/share.$slug.opengraph.tsx b/apps/remix/app/routes/_share+/share.$slug.opengraph.tsx similarity index 97% rename from apps/remix/app/routes/_unauthenticated+/share.$slug.opengraph.tsx rename to apps/remix/app/routes/_share+/share.$slug.opengraph.tsx index 2a6f7c6de..7e4cbd636 100644 --- a/apps/remix/app/routes/_unauthenticated+/share.$slug.opengraph.tsx +++ b/apps/remix/app/routes/_share+/share.$slug.opengraph.tsx @@ -22,6 +22,11 @@ const IMAGE_SIZE = { export const loader = async ({ params }: Route.LoaderArgs) => { const { slug } = params; + // QR codes are not supported for OpenGraph images + if (slug.startsWith('qr_')) { + return new Response('Not found', { status: 404 }); + } + const baseUrl = NEXT_PUBLIC_WEBAPP_URL(); const [interSemiBold, interRegular, caveatRegular] = await Promise.all([ diff --git a/apps/remix/app/routes/_unauthenticated+/share.$slug.tsx b/apps/remix/app/routes/_share+/share.$slug.tsx similarity index 57% rename from apps/remix/app/routes/_unauthenticated+/share.$slug.tsx rename to apps/remix/app/routes/_share+/share.$slug.tsx index e82ab6676..c0a3f75b2 100644 --- a/apps/remix/app/routes/_unauthenticated+/share.$slug.tsx +++ b/apps/remix/app/routes/_share+/share.$slug.tsx @@ -1,10 +1,17 @@ -import { redirect } from 'react-router'; +import { redirect, useLoaderData } from 'react-router'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; +import { getDocumentByAccessToken } from '@documenso/lib/server-only/document/get-document-by-access-token'; + +import { DocumentCertificateQRView } from '~/components/general/document/document-certificate-qr-view'; import type { Route } from './+types/share.$slug'; export function meta({ params: { slug } }: Route.MetaArgs) { + if (slug.startsWith('qr_')) { + return undefined; + } + return [ { title: 'Documenso - Share' }, { description: 'I just signed a document in style with Documenso!' }, @@ -43,11 +50,23 @@ export function meta({ params: { slug } }: Route.MetaArgs) { ]; } -export const loader = ({ request }: Route.LoaderArgs) => { +export const loader = async ({ request, params: { slug } }: Route.LoaderArgs) => { + if (slug.startsWith('qr_')) { + const document = await getDocumentByAccessToken({ token: slug }); + + if (!document) { + throw redirect('/'); + } + + return { + document, + }; + } + const userAgent = request.headers.get('User-Agent') ?? ''; if (/bot|facebookexternalhit|WhatsApp|google|bing|duckduckbot|MetaInspector/i.test(userAgent)) { - return null; + return {}; } // Is hardcoded because this whole meta is hardcoded anyway for Documenso. @@ -55,5 +74,20 @@ export const loader = ({ request }: Route.LoaderArgs) => { }; export default function SharePage() { + const { document } = useLoaderData(); + + if (document) { + return ( + + ); + } + return
; } diff --git a/packages/lib/jobs/definitions/internal/seal-document.handler.ts b/packages/lib/jobs/definitions/internal/seal-document.handler.ts index 716d4737b..5726dcf2a 100644 --- a/packages/lib/jobs/definitions/internal/seal-document.handler.ts +++ b/packages/lib/jobs/definitions/internal/seal-document.handler.ts @@ -22,6 +22,7 @@ import { ZWebhookDocumentSchema, mapDocumentToWebhookDocumentPayload, } from '../../../types/webhook-payload'; +import { prefixedId } from '../../../universal/id'; import { getFileServerSide } from '../../../universal/upload/get-file.server'; import { putPdfFileServerSide } from '../../../universal/upload/put-file.server'; import { fieldsContainUnsignedRequiredField } from '../../../utils/advanced-fields-helpers'; @@ -130,6 +131,17 @@ export const run = async ({ documentData.data = documentData.initialData; } + if (!document.qrToken) { + await prisma.document.update({ + where: { + id: document.id, + }, + data: { + qrToken: prefixedId('qr'), + }, + }); + } + const pdfData = await getFileServerSide(documentData); const certificateData = diff --git a/packages/lib/server-only/document/create-document-v2.ts b/packages/lib/server-only/document/create-document-v2.ts index 9e6aed167..77babbba4 100644 --- a/packages/lib/server-only/document/create-document-v2.ts +++ b/packages/lib/server-only/document/create-document-v2.ts @@ -13,7 +13,7 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; -import { nanoid } from '@documenso/lib/universal/id'; +import { nanoid, prefixedId } from '@documenso/lib/universal/id'; import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs'; import { prisma } from '@documenso/prisma'; import type { TCreateDocumentV2Request } from '@documenso/trpc/server/document-router/schema'; @@ -142,6 +142,7 @@ export const createDocumentV2 = async ({ const document = await tx.document.create({ data: { title, + qrToken: prefixedId('qr'), externalId: data.externalId, documentDataId, userId, diff --git a/packages/lib/server-only/document/create-document.ts b/packages/lib/server-only/document/create-document.ts index 992e3c685..8ca98ca29 100644 --- a/packages/lib/server-only/document/create-document.ts +++ b/packages/lib/server-only/document/create-document.ts @@ -13,6 +13,7 @@ import { ZWebhookDocumentSchema, mapDocumentToWebhookDocumentPayload, } from '../../types/webhook-payload'; +import { prefixedId } from '../../universal/id'; import { getFileServerSide } from '../../universal/upload/get-file.server'; import { putPdfFileServerSide } from '../../universal/upload/put-file.server'; import { determineDocumentVisibility } from '../../utils/document-visibility'; @@ -115,6 +116,7 @@ export const createDocument = async ({ const document = await tx.document.create({ data: { title, + qrToken: prefixedId('qr'), externalId, documentDataId, userId, diff --git a/packages/lib/server-only/document/duplicate-document-by-id.ts b/packages/lib/server-only/document/duplicate-document-by-id.ts index d91f0deb6..5599b0b4d 100644 --- a/packages/lib/server-only/document/duplicate-document-by-id.ts +++ b/packages/lib/server-only/document/duplicate-document-by-id.ts @@ -3,6 +3,7 @@ import { DocumentSource, type Prisma } from '@prisma/client'; import { prisma } from '@documenso/prisma'; import { AppError, AppErrorCode } from '../../errors/app-error'; +import { prefixedId } from '../../universal/id'; import { getDocumentWhereInput } from './get-document-by-id'; export interface DuplicateDocumentOptions { @@ -56,6 +57,7 @@ export const duplicateDocument = async ({ const createDocumentArguments: Prisma.DocumentCreateArgs = { data: { title: document.title, + qrToken: prefixedId('qr'), user: { connect: { id: document.userId, diff --git a/packages/lib/server-only/document/get-document-by-access-token.ts b/packages/lib/server-only/document/get-document-by-access-token.ts new file mode 100644 index 000000000..e7ccdfbf8 --- /dev/null +++ b/packages/lib/server-only/document/get-document-by-access-token.ts @@ -0,0 +1,38 @@ +import { prisma } from '@documenso/prisma'; + +export type GetDocumentByAccessTokenOptions = { + token: string; +}; + +export const getDocumentByAccessToken = async ({ token }: GetDocumentByAccessTokenOptions) => { + if (!token) { + throw new Error('Missing token'); + } + + const result = await prisma.document.findFirstOrThrow({ + where: { + qrToken: token, + }, + select: { + id: true, + title: true, + completedAt: true, + documentData: { + select: { + id: true, + type: true, + data: true, + initialData: true, + }, + }, + documentMeta: { + select: { + password: true, + }, + }, + recipients: true, + }, + }); + + return result; +}; diff --git a/packages/lib/server-only/template/create-document-from-direct-template.ts b/packages/lib/server-only/template/create-document-from-direct-template.ts index 0b8784417..9d71f3297 100644 --- a/packages/lib/server-only/template/create-document-from-direct-template.ts +++ b/packages/lib/server-only/template/create-document-from-direct-template.ts @@ -19,7 +19,7 @@ import { z } from 'zod'; import { mailer } from '@documenso/email/mailer'; import { DocumentCreatedFromDirectTemplateEmailTemplate } from '@documenso/email/templates/document-created-from-direct-template'; -import { nanoid } from '@documenso/lib/universal/id'; +import { nanoid, prefixedId } from '@documenso/lib/universal/id'; import { prisma } from '@documenso/prisma'; import type { TSignFieldWithTokenMutationSchema } from '@documenso/trpc/server/field-router/schema'; @@ -276,6 +276,7 @@ export const createDocumentFromDirectTemplate = async ({ // Create the document and non direct template recipients. const document = await tx.document.create({ data: { + qrToken: prefixedId('qr'), source: DocumentSource.TEMPLATE_DIRECT_LINK, templateId: template.id, userId: template.userId, diff --git a/packages/lib/server-only/template/create-document-from-template-legacy.ts b/packages/lib/server-only/template/create-document-from-template-legacy.ts index ea1659a1d..06aaa3c64 100644 --- a/packages/lib/server-only/template/create-document-from-template-legacy.ts +++ b/packages/lib/server-only/template/create-document-from-template-legacy.ts @@ -1,6 +1,6 @@ import { DocumentSource, type RecipientRole } from '@prisma/client'; -import { nanoid } from '@documenso/lib/universal/id'; +import { nanoid, prefixedId } from '@documenso/lib/universal/id'; import { prisma } from '@documenso/prisma'; export type CreateDocumentFromTemplateLegacyOptions = { @@ -70,6 +70,7 @@ export const createDocumentFromTemplateLegacy = async ({ const document = await prisma.document.create({ data: { + qrToken: prefixedId('qr'), source: DocumentSource.TEMPLATE, templateId: template.id, userId, diff --git a/packages/lib/server-only/template/create-document-from-template.ts b/packages/lib/server-only/template/create-document-from-template.ts index b17a36835..ebefd10cb 100644 --- a/packages/lib/server-only/template/create-document-from-template.ts +++ b/packages/lib/server-only/template/create-document-from-template.ts @@ -11,7 +11,7 @@ import { } from '@prisma/client'; import { match } from 'ts-pattern'; -import { nanoid } from '@documenso/lib/universal/id'; +import { nanoid, prefixedId } from '@documenso/lib/universal/id'; import { prisma } from '@documenso/prisma'; import type { SupportedLanguageCodes } from '../../constants/i18n'; @@ -372,6 +372,7 @@ export const createDocumentFromTemplate = async ({ return await prisma.$transaction(async (tx) => { const document = await tx.document.create({ data: { + qrToken: prefixedId('qr'), source: DocumentSource.TEMPLATE, externalId: externalId || template.externalId, templateId: template.id, diff --git a/packages/lib/types/document.ts b/packages/lib/types/document.ts index 3709b140c..0835f8ce7 100644 --- a/packages/lib/types/document.ts +++ b/packages/lib/types/document.ts @@ -86,6 +86,8 @@ export const ZDocumentLiteSchema = DocumentSchema.pick({ useLegacyFieldInsertion: true, }); +export type TDocumentLite = z.infer; + /** * A version of the document response schema when returning multiple documents at once from a single API endpoint. */ @@ -119,3 +121,5 @@ export const ZDocumentManySchema = DocumentSchema.pick({ url: true, }).nullable(), }); + +export type TDocumentMany = z.infer; diff --git a/packages/lib/universal/id.ts b/packages/lib/universal/id.ts index 0d40dd088..e2f01b044 100644 --- a/packages/lib/universal/id.ts +++ b/packages/lib/universal/id.ts @@ -3,3 +3,9 @@ import { customAlphabet } from 'nanoid'; export const alphaid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 21); export { nanoid } from 'nanoid'; + +export const fancyId = customAlphabet('abcdefhiklmnorstuvwxyz', 16); + +export const prefixedId = (prefix: string, length = 16) => { + return `${prefix}_${fancyId(length)}`; +}; diff --git a/packages/prisma/index.ts b/packages/prisma/index.ts index b9c1600b7..cffa7a451 100644 --- a/packages/prisma/index.ts +++ b/packages/prisma/index.ts @@ -31,4 +31,38 @@ export const kyselyPrisma = remember('kyselyPrisma', () => ), ); +export const prismaWithLogging = remember('prismaWithLogging', () => { + const client = new PrismaClient({ + datasourceUrl: getDatabaseUrl(), + log: [ + { + emit: 'event', + level: 'query', + }, + ], + }); + + client.$on('query', (e) => { + console.log('query:', e.query); + console.log('params:', e.params); + console.log('duration:', e.duration); + + const params = JSON.parse(e.params) as unknown[]; + + const query = e.query.replace(/\$\d+/g, (match) => { + const index = Number(match.replace('$', '')); + + if (index > params.length) { + return match; + } + + return String(params[index - 1]); + }); + + console.log('formatted query:', query); + }); + + return client; +}); + export { sql } from 'kysely'; diff --git a/packages/prisma/migrations/20250425015123_add_qr_token_to_document/migration.sql b/packages/prisma/migrations/20250425015123_add_qr_token_to_document/migration.sql new file mode 100644 index 000000000..7965ef1f2 --- /dev/null +++ b/packages/prisma/migrations/20250425015123_add_qr_token_to_document/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Document" ADD COLUMN "qrToken" TEXT; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 4eec0bbc7..0ba161545 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -315,6 +315,7 @@ enum DocumentVisibility { /// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';", "import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';"]) model Document { id Int @id @default(autoincrement()) + qrToken String? /// @zod.string.describe("The token for viewing the document using the QR code on the certificate.") externalId String? /// @zod.string.describe("A custom external ID you can use to identify the document.") userId Int /// @zod.number.describe("The ID of the user that created this document.") user User @relation(fields: [userId], references: [id], onDelete: Cascade) diff --git a/packages/trpc/server/share-link-router/get-document-internal-url-for-qr-code.ts b/packages/trpc/server/share-link-router/get-document-internal-url-for-qr-code.ts new file mode 100644 index 000000000..d1d9cdcbc --- /dev/null +++ b/packages/trpc/server/share-link-router/get-document-internal-url-for-qr-code.ts @@ -0,0 +1,61 @@ +import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; +import { prisma } from '@documenso/prisma'; + +import { procedure } from '../trpc'; +import { + ZGetDocumentInternalUrlForQRCodeInput, + ZGetDocumentInternalUrlForQRCodeOutput, +} from './get-document-internal-url-for-qr-code.types'; + +export const getDocumentInternalUrlForQRCodeRoute = procedure + .input(ZGetDocumentInternalUrlForQRCodeInput) + .output(ZGetDocumentInternalUrlForQRCodeOutput) + .query(async ({ input, ctx }) => { + const { documentId } = input; + + if (!ctx.user) { + return null; + } + + const document = await prisma.document.findFirst({ + where: { + OR: [ + { + id: documentId, + userId: ctx.user.id, + }, + { + id: documentId, + team: { + members: { + some: { + userId: ctx.user.id, + }, + }, + }, + }, + ], + }, + include: { + team: { + where: { + members: { + some: { + userId: ctx.user.id, + }, + }, + }, + }, + }, + }); + + if (!document) { + return null; + } + + if (document.team) { + return `${NEXT_PUBLIC_WEBAPP_URL()}/t/${document.team.url}/documents/${document.id}`; + } + + return `${NEXT_PUBLIC_WEBAPP_URL()}/documents/${document.id}`; + }); diff --git a/packages/trpc/server/share-link-router/get-document-internal-url-for-qr-code.types.ts b/packages/trpc/server/share-link-router/get-document-internal-url-for-qr-code.types.ts new file mode 100644 index 000000000..cc7a56c1d --- /dev/null +++ b/packages/trpc/server/share-link-router/get-document-internal-url-for-qr-code.types.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; + +export const ZGetDocumentInternalUrlForQRCodeInput = z.object({ + documentId: z.number(), +}); + +export type TGetDocumentInternalUrlForQRCodeInput = z.infer< + typeof ZGetDocumentInternalUrlForQRCodeInput +>; + +export const ZGetDocumentInternalUrlForQRCodeOutput = z.string().nullable(); + +export type TGetDocumentInternalUrlForQRCodeOutput = z.infer< + typeof ZGetDocumentInternalUrlForQRCodeOutput +>; diff --git a/packages/trpc/server/share-link-router/router.ts b/packages/trpc/server/share-link-router/router.ts index 517e207e8..f10424cae 100644 --- a/packages/trpc/server/share-link-router/router.ts +++ b/packages/trpc/server/share-link-router/router.ts @@ -1,6 +1,7 @@ import { createOrGetShareLink } from '@documenso/lib/server-only/share/create-or-get-share-link'; import { procedure, router } from '../trpc'; +import { getDocumentInternalUrlForQRCodeRoute } from './get-document-internal-url-for-qr-code'; import { ZCreateOrGetShareLinkMutationSchema } from './schema'; export const shareLinkRouter = router({ @@ -21,4 +22,6 @@ export const shareLinkRouter = router({ return await createOrGetShareLink({ documentId, userId: ctx.user.id }); }), + + getDocumentInternalUrlForQRCode: getDocumentInternalUrlForQRCodeRoute, }); From 12ada567f59c54207a5c10ed2a305e81942e5986 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Thu, 1 May 2025 23:32:56 +1000 Subject: [PATCH 03/12] feat: embed authoring part two (#1768) --- .../configure-document-recipients.tsx | 46 ++- .../authoring/configure-document-view.tsx | 10 +- .../configure-document-view.types.ts | 4 +- .../embed/authoring/configure-fields-view.tsx | 55 ++- .../authoring/configure-fields-view.types.ts | 29 ++ .../field-advanced-settings-drawer.tsx | 31 +- .../routes/embed+/v1+/authoring+/_layout.tsx | 102 ++++++ .../authoring+}/document.create.tsx | 19 +- .../v1+/authoring+/document.edit.$id.tsx | 314 ++++++++++++++++++ .../authoring+}/template.create.tsx | 10 +- .../v1+/authoring+/template.edit.$id.tsx | 314 ++++++++++++++++++ .../authoring_.completed.create.tsx} | 0 .../routes/embed+/v1.authoring+/_layout.tsx | 66 ---- packages/lib/constants/date-formats.ts | 6 + .../emails/send-team-deleted-email.ts | 1 + .../recipient/set-document-recipients.ts | 12 +- .../migration.sql | 2 + packages/prisma/schema.prisma | 2 + .../trpc/server/embedding-router/_router.ts | 6 +- .../create-embedding-presign-token.ts | 19 +- .../get-embedding-document.ts | 63 ---- .../get-embedding-document.types.ts | 34 -- .../update-embedding-document.ts | 118 +++++++ .../update-embedding-document.types.ts | 87 +++++ .../update-embedding-template.ts | 104 ++++++ .../update-embedding-template.types.ts | 77 +++++ .../trpc/server/recipient-router/schema.ts | 2 +- packages/ui/primitives/recipient-selector.tsx | 3 +- 28 files changed, 1267 insertions(+), 269 deletions(-) create mode 100644 apps/remix/app/components/embed/authoring/configure-fields-view.types.ts create mode 100644 apps/remix/app/routes/embed+/v1+/authoring+/_layout.tsx rename apps/remix/app/routes/embed+/{v1.authoring+ => v1+/authoring+}/document.create.tsx (90%) create mode 100644 apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx rename apps/remix/app/routes/embed+/{v1.authoring+ => v1+/authoring+}/template.create.tsx (95%) create mode 100644 apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx rename apps/remix/app/routes/embed+/{v1.authoring+/create-completed.tsx => v1+/authoring_.completed.create.tsx} (100%) delete mode 100644 apps/remix/app/routes/embed+/v1.authoring+/_layout.tsx create mode 100644 packages/prisma/migrations/20250501123616_add_embedded_authoring_team_flag/migration.sql delete mode 100644 packages/trpc/server/embedding-router/get-embedding-document.ts delete mode 100644 packages/trpc/server/embedding-router/get-embedding-document.types.ts create mode 100644 packages/trpc/server/embedding-router/update-embedding-document.ts create mode 100644 packages/trpc/server/embedding-router/update-embedding-document.types.ts create mode 100644 packages/trpc/server/embedding-router/update-embedding-template.ts create mode 100644 packages/trpc/server/embedding-router/update-embedding-template.types.ts diff --git a/apps/remix/app/components/embed/authoring/configure-document-recipients.tsx b/apps/remix/app/components/embed/authoring/configure-document-recipients.tsx index 06b70f3c7..918af6dd1 100644 --- a/apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +++ b/apps/remix/app/components/embed/authoring/configure-document-recipients.tsx @@ -57,7 +57,7 @@ export const ConfigureDocumentRecipients = ({ name: 'signers', }); - const { getValues, watch } = useFormContext(); + const { getValues, watch, setValue } = useFormContext(); const signingOrder = watch('meta.signingOrder'); @@ -67,13 +67,16 @@ export const ConfigureDocumentRecipients = ({ const onAddSigner = useCallback(() => { const signerNumber = signers.length + 1; + const recipientSigningOrder = + signers.length > 0 ? (signers[signers.length - 1]?.signingOrder || 0) + 1 : 1; appendSigner({ formId: nanoid(8), name: isTemplate ? `Recipient ${signerNumber}` : '', email: isTemplate ? `recipient.${signerNumber}@document.com` : '', role: RecipientRole.SIGNER, - signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder || 0) + 1 : 1, + signingOrder: + signingOrder === DocumentSigningOrder.SEQUENTIAL ? recipientSigningOrder : undefined, }); }, [appendSigner, signers]); @@ -103,7 +106,7 @@ export const ConfigureDocumentRecipients = ({ // Update signing order for each item const updatedSigners = remainingSigners.map((s: SignerItem, idx: number) => ({ ...s, - signingOrder: idx + 1, + signingOrder: signingOrder === DocumentSigningOrder.SEQUENTIAL ? idx + 1 : undefined, })); // Update the form @@ -123,7 +126,7 @@ export const ConfigureDocumentRecipients = ({ const currentSigners = getValues('signers'); const updatedSigners = currentSigners.map((signer: SignerItem, index: number) => ({ ...signer, - signingOrder: index + 1, + signingOrder: signingOrder === DocumentSigningOrder.SEQUENTIAL ? index + 1 : undefined, })); // Update the form with new ordering @@ -132,6 +135,16 @@ export const ConfigureDocumentRecipients = ({ [move, replace, getValues], ); + const onSigningOrderChange = (signingOrder: DocumentSigningOrder) => { + setValue('meta.signingOrder', signingOrder); + + if (signingOrder === DocumentSigningOrder.SEQUENTIAL) { + signers.forEach((_signer, index) => { + setValue(`signers.${index}.signingOrder`, index + 1); + }); + } + }; + return (

@@ -152,11 +165,11 @@ export const ConfigureDocumentRecipients = ({ {...field} id="signingOrder" checked={field.value === DocumentSigningOrder.SEQUENTIAL} - onCheckedChange={(checked) => { - field.onChange( + onCheckedChange={(checked) => + onSigningOrderChange( checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL, - ); - }} + ) + } disabled={isSubmitting} /> @@ -184,6 +197,7 @@ export const ConfigureDocumentRecipients = ({ disabled={isSubmitting || !isSigningOrderEnabled} /> +
{(provided, snapshot) => ( -
@@ -360,13 +375,18 @@ export const ConfigureDocumentRecipients = ({ -
+ )} ))} diff --git a/apps/remix/app/components/embed/authoring/configure-document-view.tsx b/apps/remix/app/components/embed/authoring/configure-document-view.tsx index c9e5097ce..cadd10d19 100644 --- a/apps/remix/app/components/embed/authoring/configure-document-view.tsx +++ b/apps/remix/app/components/embed/authoring/configure-document-view.tsx @@ -30,10 +30,15 @@ import { export interface ConfigureDocumentViewProps { onSubmit: (data: TConfigureEmbedFormSchema) => void | Promise; defaultValues?: Partial; + disableUpload?: boolean; isSubmitting?: boolean; } -export const ConfigureDocumentView = ({ onSubmit, defaultValues }: ConfigureDocumentViewProps) => { +export const ConfigureDocumentView = ({ + onSubmit, + defaultValues, + disableUpload, +}: ConfigureDocumentViewProps) => { const { isTemplate } = useConfigureDocument(); const form = useForm({ @@ -47,6 +52,7 @@ export const ConfigureDocumentView = ({ onSubmit, defaultValues }: ConfigureDocu email: isTemplate ? `recipient.${1}@document.com` : '', role: RecipientRole.SIGNER, signingOrder: 1, + disabled: false, }, ], meta: { @@ -110,7 +116,7 @@ export const ConfigureDocumentView = ({ onSubmit, defaultValues }: ConfigureDocu />
- + {!disableUpload && } diff --git a/apps/remix/app/components/embed/authoring/configure-document-view.types.ts b/apps/remix/app/components/embed/authoring/configure-document-view.types.ts index bbd90e4a0..42eab5442 100644 --- a/apps/remix/app/components/embed/authoring/configure-document-view.types.ts +++ b/apps/remix/app/components/embed/authoring/configure-document-view.types.ts @@ -15,11 +15,13 @@ export const ZConfigureEmbedFormSchema = z.object({ signers: z .array( z.object({ + nativeId: z.number().optional(), formId: z.string(), name: z.string().min(1, { message: 'Name is required' }), email: z.string().email('Invalid email address'), role: z.enum(['SIGNER', 'CC', 'APPROVER', 'VIEWER', 'ASSISTANT']), signingOrder: z.number().optional(), + disabled: z.boolean().optional(), }), ) .min(1, { message: 'At least one signer is required' }), @@ -34,7 +36,7 @@ export const ZConfigureEmbedFormSchema = z.object({ language: ZDocumentMetaLanguageSchema.optional(), signatureTypes: z.array(z.string()).default([]), signingOrder: z.enum(['SEQUENTIAL', 'PARALLEL']), - allowDictateNextSigner: z.boolean().default(false), + allowDictateNextSigner: z.boolean().default(false).optional(), externalId: z.string().optional(), }), documentData: z diff --git a/apps/remix/app/components/embed/authoring/configure-fields-view.tsx b/apps/remix/app/components/embed/authoring/configure-fields-view.tsx index 68d833d38..524eda381 100644 --- a/apps/remix/app/components/embed/authoring/configure-fields-view.tsx +++ b/apps/remix/app/components/embed/authoring/configure-fields-view.tsx @@ -2,12 +2,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import type { DocumentData } from '@prisma/client'; -import { FieldType, ReadStatus, type Recipient, SendStatus, SigningStatus } from '@prisma/client'; +import type { DocumentData, FieldType } from '@prisma/client'; +import { ReadStatus, type Recipient, SendStatus, SigningStatus } from '@prisma/client'; import { ChevronsUpDown } from 'lucide-react'; import { useFieldArray, useForm } from 'react-hook-form'; import { useHotkeys } from 'react-hotkeys-hook'; -import { z } from 'zod'; import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect'; import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element'; @@ -30,6 +29,7 @@ import { Sheet, SheetContent, SheetTrigger } from '@documenso/ui/primitives/shee import { useToast } from '@documenso/ui/primitives/use-toast'; import type { TConfigureEmbedFormSchema } from './configure-document-view.types'; +import type { TConfigureFieldsFormSchema } from './configure-fields-view.types'; import { FieldAdvancedSettingsDrawer } from './field-advanced-settings-drawer'; const MIN_HEIGHT_PX = 12; @@ -38,28 +38,9 @@ const MIN_WIDTH_PX = 36; const DEFAULT_HEIGHT_PX = MIN_HEIGHT_PX * 2.5; const DEFAULT_WIDTH_PX = MIN_WIDTH_PX * 2.5; -export const ZConfigureFieldsFormSchema = z.object({ - fields: z.array( - z.object({ - formId: z.string().min(1), - id: z.string().min(1), - type: z.nativeEnum(FieldType), - signerEmail: z.string().min(1), - recipientId: z.number().min(0), - pageNumber: z.number().min(1), - pageX: z.number().min(0), - pageY: z.number().min(0), - pageWidth: z.number().min(0), - pageHeight: z.number().min(0), - fieldMeta: ZFieldMetaSchema.optional(), - }), - ), -}); - -export type TConfigureFieldsFormSchema = z.infer; - export type ConfigureFieldsViewProps = { configData: TConfigureEmbedFormSchema; + documentData?: DocumentData; defaultValues?: Partial; onBack: (data: TConfigureFieldsFormSchema) => void; onSubmit: (data: TConfigureFieldsFormSchema) => void; @@ -67,13 +48,14 @@ export type ConfigureFieldsViewProps = { export const ConfigureFieldsView = ({ configData, + documentData, defaultValues, onBack, onSubmit, }: ConfigureFieldsViewProps) => { + const { _ } = useLingui(); const { toast } = useToast(); const { isWithinPageBounds, getFieldPosition, getPage } = useDocumentElement(); - const { _ } = useLingui(); // Track if we're on a mobile device const [isMobile, setIsMobile] = useState(false); @@ -99,7 +81,11 @@ export const ConfigureFieldsView = ({ }; }, []); - const documentData = useMemo(() => { + const normalizedDocumentData = useMemo(() => { + if (documentData) { + return documentData; + } + if (!configData.documentData) { return null; } @@ -116,7 +102,7 @@ export const ConfigureFieldsView = ({ const recipients = useMemo(() => { return configData.signers.map((signer, index) => ({ - id: index, + id: signer.nativeId || index, name: signer.name || '', email: signer.email || '', role: signer.role, @@ -129,14 +115,14 @@ export const ConfigureFieldsView = ({ signedAt: null, authOptions: null, rejectionReason: null, - sendStatus: SendStatus.NOT_SENT, - readStatus: ReadStatus.NOT_OPENED, - signingStatus: SigningStatus.NOT_SIGNED, + sendStatus: signer.disabled ? SendStatus.SENT : SendStatus.NOT_SENT, + readStatus: signer.disabled ? ReadStatus.OPENED : ReadStatus.NOT_OPENED, + signingStatus: signer.disabled ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED, })); }, [configData.signers]); const [selectedRecipient, setSelectedRecipient] = useState( - () => recipients[0] || null, + () => recipients.find((r) => r.signingStatus === SigningStatus.NOT_SIGNED) || null, ); const [selectedField, setSelectedField] = useState(null); const [isFieldWithinBounds, setIsFieldWithinBounds] = useState(false); @@ -206,8 +192,8 @@ export const ConfigureFieldsView = ({ const newField: TConfigureFieldsFormSchema['fields'][0] = { ...structuredClone(lastActiveField), + nativeId: undefined, formId: nanoid(12), - id: nanoid(12), signerEmail: selectedRecipient?.email ?? lastActiveField.signerEmail, recipientId: selectedRecipient?.id ?? lastActiveField.recipientId, pageX: lastActiveField.pageX + 3, @@ -229,8 +215,8 @@ export const ConfigureFieldsView = ({ append({ ...copiedField, + nativeId: undefined, formId: nanoid(12), - id: nanoid(12), signerEmail: selectedRecipient?.email ?? copiedField.signerEmail, recipientId: selectedRecipient?.id ?? copiedField.recipientId, pageX: copiedField.pageX + 3, @@ -303,7 +289,6 @@ export const ConfigureFieldsView = ({ pageY -= fieldPageHeight / 2; const field = { - id: nanoid(12), formId: nanoid(12), type: selectedField, pageNumber, @@ -526,9 +511,9 @@ export const ConfigureFieldsView = ({ )}
- {documentData && ( + {normalizedDocumentData && (
- + {localFields.map((field, index) => { diff --git a/apps/remix/app/components/embed/authoring/configure-fields-view.types.ts b/apps/remix/app/components/embed/authoring/configure-fields-view.types.ts new file mode 100644 index 000000000..b0738bb0a --- /dev/null +++ b/apps/remix/app/components/embed/authoring/configure-fields-view.types.ts @@ -0,0 +1,29 @@ +import { z } from 'zod'; + +import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta'; +import { FieldType } from '@documenso/prisma/client'; + +export const ZConfigureFieldsFormSchema = z.object({ + fields: z.array( + z.object({ + nativeId: z.number().optional(), + formId: z.string().min(1), + type: z.nativeEnum(FieldType), + signerEmail: z.string().min(1), + inserted: z.boolean().optional(), + recipientId: z.number().min(0), + pageNumber: z.number().min(1), + pageX: z.number().min(0), + pageY: z.number().min(0), + pageWidth: z.number().min(0), + pageHeight: z.number().min(0), + fieldMeta: ZFieldMetaSchema.optional(), + }), + ), +}); + +export type TConfigureFieldsFormSchema = z.infer; + +export type TConfigureFieldsFormSchemaField = z.infer< + typeof ZConfigureFieldsFormSchema +>['fields'][number]; diff --git a/apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx b/apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx index 6646586a7..2a20076d4 100644 --- a/apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx +++ b/apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx @@ -1,6 +1,5 @@ import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; -import type { FieldType } from '@prisma/client'; import { type TFieldMetaSchema as FieldMeta } from '@documenso/lib/types/field-meta'; import { parseMessageDescriptor } from '@documenso/lib/utils/i18n'; @@ -8,35 +7,13 @@ import { FieldAdvancedSettings } from '@documenso/ui/primitives/document-flow/fi import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types'; import { Sheet, SheetContent, SheetTitle } from '@documenso/ui/primitives/sheet'; +import type { TConfigureFieldsFormSchemaField } from './configure-fields-view.types'; + export type FieldAdvancedSettingsDrawerProps = { isOpen: boolean; onOpenChange: (isOpen: boolean) => void; - currentField: { - id: string; - formId: string; - type: FieldType; - pageNumber: number; - pageX: number; - pageY: number; - pageWidth: number; - pageHeight: number; - recipientId: number; - signerEmail: string; - fieldMeta?: FieldMeta; - } | null; - fields: Array<{ - id: string; - formId: string; - type: FieldType; - pageNumber: number; - pageX: number; - pageY: number; - pageWidth: number; - pageHeight: number; - recipientId: number; - signerEmail: string; - fieldMeta?: FieldMeta; - }>; + currentField: TConfigureFieldsFormSchemaField | null; + fields: TConfigureFieldsFormSchemaField[]; onFieldUpdate: (formId: string, fieldMeta: FieldMeta) => void; }; diff --git a/apps/remix/app/routes/embed+/v1+/authoring+/_layout.tsx b/apps/remix/app/routes/embed+/v1+/authoring+/_layout.tsx new file mode 100644 index 000000000..4d6b7e729 --- /dev/null +++ b/apps/remix/app/routes/embed+/v1+/authoring+/_layout.tsx @@ -0,0 +1,102 @@ +import { useLayoutEffect } from 'react'; + +import { Outlet, useLoaderData } from 'react-router'; + +import { isCommunityPlan } from '@documenso/ee/server-only/util/is-community-plan'; +import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise'; +import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform'; +import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token'; +import { TrpcProvider } from '@documenso/trpc/react'; + +import { ZBaseEmbedAuthoringSchema } from '~/types/embed-authoring-base-schema'; +import { injectCss } from '~/utils/css-vars'; + +import type { Route } from './+types/_layout'; + +export const loader = async ({ request }: Route.LoaderArgs) => { + const url = new URL(request.url); + + const token = url.searchParams.get('token'); + + if (!token) { + return { + hasValidToken: false, + token, + }; + } + + const result = await verifyEmbeddingPresignToken({ token }).catch(() => null); + + let hasPlatformPlan = false; + let hasEnterprisePlan = false; + let hasCommunityPlan = false; + + if (result) { + [hasCommunityPlan, hasPlatformPlan, hasEnterprisePlan] = await Promise.all([ + isCommunityPlan({ + userId: result.userId, + teamId: result.teamId ?? undefined, + }), + isDocumentPlatform({ + userId: result.userId, + teamId: result.teamId, + }), + isUserEnterprise({ + userId: result.userId, + teamId: result.teamId ?? undefined, + }), + ]); + } + + return { + hasValidToken: !!result, + token, + hasCommunityPlan, + hasPlatformPlan, + hasEnterprisePlan, + }; +}; + +export default function AuthoringLayout() { + const { hasValidToken, token, hasCommunityPlan, hasPlatformPlan, hasEnterprisePlan } = + useLoaderData(); + + useLayoutEffect(() => { + try { + const hash = window.location.hash.slice(1); + + const result = ZBaseEmbedAuthoringSchema.safeParse( + JSON.parse(decodeURIComponent(atob(hash))), + ); + + if (!result.success) { + return; + } + + const { css, cssVars, darkModeDisabled } = result.data; + + if (darkModeDisabled) { + document.documentElement.classList.add('dark-mode-disabled'); + } + + if (hasCommunityPlan || hasPlatformPlan || hasEnterprisePlan) { + injectCss({ + css, + cssVars, + }); + } + } catch (error) { + console.error(error); + } + }, []); + + if (!hasValidToken) { + return
Invalid embedding presign token provided
; + } + + return ( + + + + ); +} diff --git a/apps/remix/app/routes/embed+/v1.authoring+/document.create.tsx b/apps/remix/app/routes/embed+/v1+/authoring+/document.create.tsx similarity index 90% rename from apps/remix/app/routes/embed+/v1.authoring+/document.create.tsx rename to apps/remix/app/routes/embed+/v1+/authoring+/document.create.tsx index e80b1558c..bf3c09e63 100644 --- a/apps/remix/app/routes/embed+/v1.authoring+/document.create.tsx +++ b/apps/remix/app/routes/embed+/v1+/authoring+/document.create.tsx @@ -12,10 +12,8 @@ import { useToast } from '@documenso/ui/primitives/use-toast'; import { ConfigureDocumentProvider } from '~/components/embed/authoring/configure-document-context'; import { ConfigureDocumentView } from '~/components/embed/authoring/configure-document-view'; import type { TConfigureEmbedFormSchema } from '~/components/embed/authoring/configure-document-view.types'; -import { - ConfigureFieldsView, - type TConfigureFieldsFormSchema, -} from '~/components/embed/authoring/configure-fields-view'; +import { ConfigureFieldsView } from '~/components/embed/authoring/configure-fields-view'; +import type { TConfigureFieldsFormSchema } from '~/components/embed/authoring/configure-fields-view.types'; import { type TBaseEmbedAuthoringSchema, ZBaseEmbedAuthoringSchema, @@ -71,6 +69,8 @@ export default function EmbeddingAuthoringDocumentCreatePage() { // Use the externalId from the URL fragment if available const documentExternalId = externalId || configuration.meta.externalId; + const signatureTypes = configuration.meta.signatureTypes ?? []; + const createResult = await createEmbeddingDocument({ title: configuration.title, documentDataId: documentData.id, @@ -78,14 +78,11 @@ export default function EmbeddingAuthoringDocumentCreatePage() { meta: { ...configuration.meta, drawSignatureEnabled: - configuration.meta.signatureTypes.length === 0 || - configuration.meta.signatureTypes.includes(DocumentSignatureType.DRAW), + signatureTypes.length === 0 || signatureTypes.includes(DocumentSignatureType.DRAW), typedSignatureEnabled: - configuration.meta.signatureTypes.length === 0 || - configuration.meta.signatureTypes.includes(DocumentSignatureType.TYPE), + signatureTypes.length === 0 || signatureTypes.includes(DocumentSignatureType.TYPE), uploadSignatureEnabled: - configuration.meta.signatureTypes.length === 0 || - configuration.meta.signatureTypes.includes(DocumentSignatureType.UPLOAD), + signatureTypes.length === 0 || signatureTypes.includes(DocumentSignatureType.UPLOAD), }, recipients: configuration.signers.map((signer) => ({ name: signer.name, @@ -126,7 +123,7 @@ export default function EmbeddingAuthoringDocumentCreatePage() { // Navigate to the completion page instead of the document details page await navigate( - `/embed/v1/authoring/create-completed?documentId=${createResult.documentId}&externalId=${documentExternalId}#${hash}`, + `/embed/v1/authoring/completed/create?documentId=${createResult.documentId}&externalId=${documentExternalId}#${hash}`, ); } catch (err) { console.error('Error creating document:', err); diff --git a/apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx b/apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx new file mode 100644 index 000000000..41b97e654 --- /dev/null +++ b/apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx @@ -0,0 +1,314 @@ +import { useLayoutEffect, useMemo, useState } from 'react'; + +import { useLingui } from '@lingui/react'; +import { DocumentDistributionMethod, DocumentSigningOrder, SigningStatus } from '@prisma/client'; +import { redirect, useLoaderData } from 'react-router'; + +import { + DEFAULT_DOCUMENT_DATE_FORMAT, + isValidDateFormat, +} from '@documenso/lib/constants/date-formats'; +import { DocumentSignatureType } from '@documenso/lib/constants/document'; +import { isValidLanguageCode } from '@documenso/lib/constants/i18n'; +import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; +import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id'; +import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token'; +import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; +import { nanoid } from '@documenso/lib/universal/id'; +import { trpc } from '@documenso/trpc/react'; +import { Stepper } from '@documenso/ui/primitives/stepper'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +import { ConfigureDocumentProvider } from '~/components/embed/authoring/configure-document-context'; +import { ConfigureDocumentView } from '~/components/embed/authoring/configure-document-view'; +import type { TConfigureEmbedFormSchema } from '~/components/embed/authoring/configure-document-view.types'; +import { ConfigureFieldsView } from '~/components/embed/authoring/configure-fields-view'; +import type { TConfigureFieldsFormSchema } from '~/components/embed/authoring/configure-fields-view.types'; +import { + type TBaseEmbedAuthoringSchema, + ZBaseEmbedAuthoringSchema, +} from '~/types/embed-authoring-base-schema'; + +import type { Route } from './+types/document.edit.$id'; + +export const loader = async ({ request, params }: Route.LoaderArgs) => { + const { id } = params; + + const url = new URL(request.url); + + // We know that the token is present because we're checking it in the parent _layout route + const token = url.searchParams.get('token') || ''; + + // We also know that the token is valid, but we need the userId + teamId + const result = await verifyEmbeddingPresignToken({ token }).catch(() => null); + + if (!result) { + throw new Error('Invalid token'); + } + + const documentId = Number(id); + + if (!documentId || Number.isNaN(documentId)) { + redirect(`/embed/v1/authoring/error/not-found?documentId=${documentId}`); + } + + const document = await getDocumentWithDetailsById({ + documentId, + userId: result?.userId, + teamId: result?.teamId ?? undefined, + }).catch(() => null); + + if (!document) { + throw redirect(`/embed/v1/authoring/error/not-found?documentId=${documentId}`); + } + + const fields = document.fields.map((field) => ({ + ...field, + positionX: field.positionX.toNumber(), + positionY: field.positionY.toNumber(), + width: field.width.toNumber(), + height: field.height.toNumber(), + })); + + return { + document: { + ...document, + fields, + }, + }; +}; + +export default function EmbeddingAuthoringDocumentEditPage() { + const { _ } = useLingui(); + const { toast } = useToast(); + + const { document } = useLoaderData(); + + const signatureTypes = useMemo(() => { + const types: string[] = []; + + if (document.documentMeta?.drawSignatureEnabled) { + types.push(DocumentSignatureType.DRAW); + } + + if (document.documentMeta?.typedSignatureEnabled) { + types.push(DocumentSignatureType.TYPE); + } + + if (document.documentMeta?.uploadSignatureEnabled) { + types.push(DocumentSignatureType.UPLOAD); + } + + return types; + }, [document.documentMeta]); + + const [configuration, setConfiguration] = useState(() => ({ + title: document.title, + documentData: undefined, + meta: { + subject: document.documentMeta?.subject ?? undefined, + message: document.documentMeta?.message ?? undefined, + distributionMethod: + document.documentMeta?.distributionMethod ?? DocumentDistributionMethod.EMAIL, + emailSettings: document.documentMeta?.emailSettings ?? ZDocumentEmailSettingsSchema.parse({}), + timezone: document.documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE, + signingOrder: document.documentMeta?.signingOrder ?? DocumentSigningOrder.PARALLEL, + allowDictateNextSigner: document.documentMeta?.allowDictateNextSigner ?? false, + language: isValidLanguageCode(document.documentMeta?.language) + ? document.documentMeta.language + : undefined, + signatureTypes: signatureTypes, + dateFormat: isValidDateFormat(document.documentMeta?.dateFormat) + ? document.documentMeta?.dateFormat + : DEFAULT_DOCUMENT_DATE_FORMAT, + redirectUrl: document.documentMeta?.redirectUrl ?? undefined, + }, + signers: document.recipients.map((recipient) => ({ + nativeId: recipient.id, + formId: nanoid(8), + name: recipient.name, + email: recipient.email, + role: recipient.role, + signingOrder: recipient.signingOrder ?? undefined, + disabled: recipient.signingStatus !== SigningStatus.NOT_SIGNED, + })), + })); + + const [fields, setFields] = useState(() => ({ + fields: document.fields.map((field) => ({ + nativeId: field.id, + formId: nanoid(8), + type: field.type, + signerEmail: + document.recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '', + inserted: field.inserted, + recipientId: field.recipientId, + pageNumber: field.page, + pageX: field.positionX, + pageY: field.positionY, + pageWidth: field.width, + pageHeight: field.height, + fieldMeta: field.fieldMeta ?? undefined, + })), + })); + + const [features, setFeatures] = useState(null); + const [externalId, setExternalId] = useState(null); + const [currentStep, setCurrentStep] = useState(1); + + const { mutateAsync: updateEmbeddingDocument } = + trpc.embeddingPresign.updateEmbeddingDocument.useMutation(); + + const handleConfigurePageViewSubmit = (data: TConfigureEmbedFormSchema) => { + // Store the configuration data and move to the field placement stage + setConfiguration(data); + setFields((fieldData) => { + if (!fieldData) { + return fieldData; + } + + const signerEmails = data.signers.map((signer) => signer.email); + + return { + fields: fieldData.fields.filter((field) => signerEmails.includes(field.signerEmail)), + }; + }); + setCurrentStep(2); + }; + + const handleBackToConfig = (data: TConfigureFieldsFormSchema) => { + // Return to the configuration view but keep the data + setFields(data); + setCurrentStep(1); + }; + + const handleConfigureFieldsSubmit = async (data: TConfigureFieldsFormSchema) => { + try { + if (!configuration) { + toast({ + variant: 'destructive', + title: _('Error'), + description: _('Please configure the document first'), + }); + + return; + } + + const fields = data.fields; + + // Use the externalId from the URL fragment if available + const documentExternalId = externalId || configuration.meta.externalId; + + const updateResult = await updateEmbeddingDocument({ + documentId: document.id, + title: configuration.title, + externalId: documentExternalId, + meta: { + ...configuration.meta, + drawSignatureEnabled: configuration.meta.signatureTypes + ? configuration.meta.signatureTypes.length === 0 || + configuration.meta.signatureTypes.includes(DocumentSignatureType.DRAW) + : undefined, + typedSignatureEnabled: configuration.meta.signatureTypes + ? configuration.meta.signatureTypes.length === 0 || + configuration.meta.signatureTypes.includes(DocumentSignatureType.TYPE) + : undefined, + uploadSignatureEnabled: configuration.meta.signatureTypes + ? configuration.meta.signatureTypes.length === 0 || + configuration.meta.signatureTypes.includes(DocumentSignatureType.UPLOAD) + : undefined, + }, + recipients: configuration.signers.map((signer) => ({ + id: signer.nativeId, + name: signer.name, + email: signer.email, + role: signer.role, + signingOrder: signer.signingOrder, + fields: fields + .filter((field) => field.signerEmail === signer.email) + // There's a gnarly discriminated union that makes this hard to satisfy, we're casting for the second + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .map((f) => ({ + ...f, + id: f.nativeId, + pageX: f.pageX, + pageY: f.pageY, + width: f.pageWidth, + height: f.pageHeight, + })), + })), + }); + + toast({ + title: _('Success'), + description: _('Document updated successfully'), + }); + + // Send a message to the parent window with the document details + if (window.parent !== window) { + window.parent.postMessage( + { + type: 'document-updated', + // documentId: updateResult.documentId, + documentId: 1, + externalId: documentExternalId, + }, + '*', + ); + } + } catch (err) { + console.error('Error updating document:', err); + + toast({ + variant: 'destructive', + title: _('Error'), + description: _('Failed to update document'), + }); + } + }; + + useLayoutEffect(() => { + try { + const hash = window.location.hash.slice(1); + + const result = ZBaseEmbedAuthoringSchema.safeParse( + JSON.parse(decodeURIComponent(atob(hash))), + ); + + if (!result.success) { + return; + } + + setFeatures(result.data.features); + + // Extract externalId from the parsed data if available + if (result.data.externalId) { + setExternalId(result.data.externalId); + } + } catch (err) { + console.error('Error parsing embedding params:', err); + } + }, []); + + return ( +
+ + + + + + + +
+ ); +} diff --git a/apps/remix/app/routes/embed+/v1.authoring+/template.create.tsx b/apps/remix/app/routes/embed+/v1+/authoring+/template.create.tsx similarity index 95% rename from apps/remix/app/routes/embed+/v1.authoring+/template.create.tsx rename to apps/remix/app/routes/embed+/v1+/authoring+/template.create.tsx index 252f6b473..372bc7265 100644 --- a/apps/remix/app/routes/embed+/v1.authoring+/template.create.tsx +++ b/apps/remix/app/routes/embed+/v1+/authoring+/template.create.tsx @@ -11,10 +11,8 @@ import { useToast } from '@documenso/ui/primitives/use-toast'; import { ConfigureDocumentProvider } from '~/components/embed/authoring/configure-document-context'; import { ConfigureDocumentView } from '~/components/embed/authoring/configure-document-view'; import type { TConfigureEmbedFormSchema } from '~/components/embed/authoring/configure-document-view.types'; -import { - ConfigureFieldsView, - type TConfigureFieldsFormSchema, -} from '~/components/embed/authoring/configure-fields-view'; +import { ConfigureFieldsView } from '~/components/embed/authoring/configure-fields-view'; +import type { TConfigureFieldsFormSchema } from '~/components/embed/authoring/configure-fields-view.types'; import { type TBaseEmbedAuthoringSchema, ZBaseEmbedAuthoringSchema, @@ -48,8 +46,6 @@ export default function EmbeddingAuthoringTemplateCreatePage() { const handleConfigureFieldsSubmit = async (data: TConfigureFieldsFormSchema) => { try { - console.log('configuration', configuration); - console.log('data', data); if (!configuration || !configuration.documentData) { toast({ variant: 'destructive', @@ -117,7 +113,7 @@ export default function EmbeddingAuthoringTemplateCreatePage() { // Navigate to the completion page instead of the template details page await navigate( - `/embed/v1/authoring/create-completed?templateId=${createResult.templateId}&externalId=${metaWithExternalId.externalId}#${hash}`, + `/embed/v1/authoring/completed/create?templateId=${createResult.templateId}&externalId=${metaWithExternalId.externalId}#${hash}`, ); } catch (err) { console.error('Error creating template:', err); diff --git a/apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx b/apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx new file mode 100644 index 000000000..7c5cef644 --- /dev/null +++ b/apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx @@ -0,0 +1,314 @@ +import { useLayoutEffect, useMemo, useState } from 'react'; + +import { useLingui } from '@lingui/react'; +import { DocumentDistributionMethod, DocumentSigningOrder, SigningStatus } from '@prisma/client'; +import { redirect, useLoaderData } from 'react-router'; + +import { + DEFAULT_DOCUMENT_DATE_FORMAT, + isValidDateFormat, +} from '@documenso/lib/constants/date-formats'; +import { DocumentSignatureType } from '@documenso/lib/constants/document'; +import { isValidLanguageCode } from '@documenso/lib/constants/i18n'; +import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; +import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token'; +import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id'; +import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; +import { nanoid } from '@documenso/lib/universal/id'; +import { trpc } from '@documenso/trpc/react'; +import { Stepper } from '@documenso/ui/primitives/stepper'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +import { ConfigureDocumentProvider } from '~/components/embed/authoring/configure-document-context'; +import { ConfigureDocumentView } from '~/components/embed/authoring/configure-document-view'; +import type { TConfigureEmbedFormSchema } from '~/components/embed/authoring/configure-document-view.types'; +import { ConfigureFieldsView } from '~/components/embed/authoring/configure-fields-view'; +import type { TConfigureFieldsFormSchema } from '~/components/embed/authoring/configure-fields-view.types'; +import { + type TBaseEmbedAuthoringSchema, + ZBaseEmbedAuthoringSchema, +} from '~/types/embed-authoring-base-schema'; + +import type { Route } from './+types/document.edit.$id'; + +export const loader = async ({ request, params }: Route.LoaderArgs) => { + const { id } = params; + + const url = new URL(request.url); + + // We know that the token is present because we're checking it in the parent _layout route + const token = url.searchParams.get('token') || ''; + + // We also know that the token is valid, but we need the userId + teamId + const result = await verifyEmbeddingPresignToken({ token }).catch(() => null); + + if (!result) { + throw new Error('Invalid token'); + } + + const templateId = Number(id); + + if (!templateId || Number.isNaN(templateId)) { + redirect(`/embed/v1/authoring/error/not-found?templateId=${templateId}`); + } + + const template = await getTemplateById({ + id: templateId, + userId: result?.userId, + teamId: result?.teamId ?? undefined, + }).catch(() => null); + + if (!template) { + throw redirect(`/embed/v1/authoring/error/not-found?templateId=${templateId}`); + } + + const fields = template.fields.map((field) => ({ + ...field, + positionX: field.positionX.toNumber(), + positionY: field.positionY.toNumber(), + width: field.width.toNumber(), + height: field.height.toNumber(), + })); + + return { + template: { + ...template, + fields, + }, + }; +}; + +export default function EmbeddingAuthoringTemplateEditPage() { + const { _ } = useLingui(); + const { toast } = useToast(); + + const { template } = useLoaderData(); + + const signatureTypes = useMemo(() => { + const types: string[] = []; + + if (template.templateMeta?.drawSignatureEnabled) { + types.push(DocumentSignatureType.DRAW); + } + + if (template.templateMeta?.typedSignatureEnabled) { + types.push(DocumentSignatureType.TYPE); + } + + if (template.templateMeta?.uploadSignatureEnabled) { + types.push(DocumentSignatureType.UPLOAD); + } + + return types; + }, [template.templateMeta]); + + const [configuration, setConfiguration] = useState(() => ({ + title: template.title, + documentData: undefined, + meta: { + subject: template.templateMeta?.subject ?? undefined, + message: template.templateMeta?.message ?? undefined, + distributionMethod: + template.templateMeta?.distributionMethod ?? DocumentDistributionMethod.EMAIL, + emailSettings: template.templateMeta?.emailSettings ?? ZDocumentEmailSettingsSchema.parse({}), + timezone: template.templateMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE, + signingOrder: template.templateMeta?.signingOrder ?? DocumentSigningOrder.PARALLEL, + allowDictateNextSigner: template.templateMeta?.allowDictateNextSigner ?? false, + language: isValidLanguageCode(template.templateMeta?.language) + ? template.templateMeta.language + : undefined, + signatureTypes: signatureTypes, + dateFormat: isValidDateFormat(template.templateMeta?.dateFormat) + ? template.templateMeta?.dateFormat + : DEFAULT_DOCUMENT_DATE_FORMAT, + redirectUrl: template.templateMeta?.redirectUrl ?? undefined, + }, + signers: template.recipients.map((recipient) => ({ + nativeId: recipient.id, + formId: nanoid(8), + name: recipient.name, + email: recipient.email, + role: recipient.role, + signingOrder: recipient.signingOrder ?? undefined, + disabled: recipient.signingStatus !== SigningStatus.NOT_SIGNED, + })), + })); + + const [fields, setFields] = useState(() => ({ + fields: template.fields.map((field) => ({ + nativeId: field.id, + formId: nanoid(8), + type: field.type, + signerEmail: + template.recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '', + inserted: field.inserted, + recipientId: field.recipientId, + pageNumber: field.page, + pageX: field.positionX, + pageY: field.positionY, + pageWidth: field.width, + pageHeight: field.height, + fieldMeta: field.fieldMeta ?? undefined, + })), + })); + + const [features, setFeatures] = useState(null); + const [externalId, setExternalId] = useState(null); + const [currentStep, setCurrentStep] = useState(1); + + const { mutateAsync: updateEmbeddingTemplate } = + trpc.embeddingPresign.updateEmbeddingTemplate.useMutation(); + + const handleConfigurePageViewSubmit = (data: TConfigureEmbedFormSchema) => { + // Store the configuration data and move to the field placement stage + setConfiguration(data); + setFields((fieldData) => { + if (!fieldData) { + return fieldData; + } + + const signerEmails = data.signers.map((signer) => signer.email); + + return { + fields: fieldData.fields.filter((field) => signerEmails.includes(field.signerEmail)), + }; + }); + setCurrentStep(2); + }; + + const handleBackToConfig = (data: TConfigureFieldsFormSchema) => { + // Return to the configuration view but keep the data + setFields(data); + setCurrentStep(1); + }; + + const handleConfigureFieldsSubmit = async (data: TConfigureFieldsFormSchema) => { + try { + if (!configuration) { + toast({ + variant: 'destructive', + title: _('Error'), + description: _('Please configure the document first'), + }); + + return; + } + + const fields = data.fields; + + // Use the externalId from the URL fragment if available + const templateExternalId = externalId || configuration.meta.externalId; + + const updateResult = await updateEmbeddingTemplate({ + templateId: template.id, + title: configuration.title, + externalId: templateExternalId, + meta: { + ...configuration.meta, + drawSignatureEnabled: configuration.meta.signatureTypes + ? configuration.meta.signatureTypes.length === 0 || + configuration.meta.signatureTypes.includes(DocumentSignatureType.DRAW) + : undefined, + typedSignatureEnabled: configuration.meta.signatureTypes + ? configuration.meta.signatureTypes.length === 0 || + configuration.meta.signatureTypes.includes(DocumentSignatureType.TYPE) + : undefined, + uploadSignatureEnabled: configuration.meta.signatureTypes + ? configuration.meta.signatureTypes.length === 0 || + configuration.meta.signatureTypes.includes(DocumentSignatureType.UPLOAD) + : undefined, + }, + recipients: configuration.signers.map((signer) => ({ + id: signer.nativeId, + name: signer.name, + email: signer.email, + role: signer.role, + signingOrder: signer.signingOrder, + fields: fields + .filter((field) => field.signerEmail === signer.email) + // There's a gnarly discriminated union that makes this hard to satisfy, we're casting for the second + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .map((f) => ({ + ...f, + id: f.nativeId, + pageX: f.pageX, + pageY: f.pageY, + width: f.pageWidth, + height: f.pageHeight, + })), + })), + }); + + toast({ + title: _('Success'), + description: _('Document updated successfully'), + }); + + // Send a message to the parent window with the template details + if (window.parent !== window) { + window.parent.postMessage( + { + type: 'template-updated', + // templateId: updateResult.templateId, + templateId: 1, + externalId: templateExternalId, + }, + '*', + ); + } + } catch (err) { + console.error('Error updating template:', err); + + toast({ + variant: 'destructive', + title: _('Error'), + description: _('Failed to update template'), + }); + } + }; + + useLayoutEffect(() => { + try { + const hash = window.location.hash.slice(1); + + const result = ZBaseEmbedAuthoringSchema.safeParse( + JSON.parse(decodeURIComponent(atob(hash))), + ); + + if (!result.success) { + return; + } + + setFeatures(result.data.features); + + // Extract externalId from the parsed data if available + if (result.data.externalId) { + setExternalId(result.data.externalId); + } + } catch (err) { + console.error('Error parsing embedding params:', err); + } + }, []); + + return ( +
+ + + + + + + +
+ ); +} diff --git a/apps/remix/app/routes/embed+/v1.authoring+/create-completed.tsx b/apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx similarity index 100% rename from apps/remix/app/routes/embed+/v1.authoring+/create-completed.tsx rename to apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx diff --git a/apps/remix/app/routes/embed+/v1.authoring+/_layout.tsx b/apps/remix/app/routes/embed+/v1.authoring+/_layout.tsx deleted file mode 100644 index fb03d91b6..000000000 --- a/apps/remix/app/routes/embed+/v1.authoring+/_layout.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { useLayoutEffect, useState } from 'react'; - -import { Outlet } from 'react-router'; - -import { TrpcProvider, trpc } from '@documenso/trpc/react'; - -import { EmbedClientLoading } from '~/components/embed/embed-client-loading'; -import { ZBaseEmbedAuthoringSchema } from '~/types/embed-authoring-base-schema'; -import { injectCss } from '~/utils/css-vars'; - -export default function AuthoringLayout() { - const [token, setToken] = useState(''); - - const { - mutateAsync: verifyEmbeddingPresignToken, - isPending: isVerifyingEmbeddingPresignToken, - data: isVerified, - } = trpc.embeddingPresign.verifyEmbeddingPresignToken.useMutation(); - - useLayoutEffect(() => { - try { - const hash = window.location.hash.slice(1); - - const result = ZBaseEmbedAuthoringSchema.safeParse( - JSON.parse(decodeURIComponent(atob(hash))), - ); - - if (!result.success) { - return; - } - - const { token, css, cssVars, darkModeDisabled } = result.data; - - if (darkModeDisabled) { - document.documentElement.classList.add('dark-mode-disabled'); - } - - injectCss({ - css, - cssVars, - }); - - void verifyEmbeddingPresignToken({ token }).then((result) => { - if (result.success) { - setToken(token); - } - }); - } catch (err) { - console.error('Error verifying embedding presign token:', err); - } - }, []); - - if (isVerifyingEmbeddingPresignToken) { - return ; - } - - if (typeof isVerified !== 'undefined' && !isVerified.success) { - return
Invalid embedding presign token
; - } - - return ( - - - - ); -} diff --git a/packages/lib/constants/date-formats.ts b/packages/lib/constants/date-formats.ts index f723f2b3b..2a3855586 100644 --- a/packages/lib/constants/date-formats.ts +++ b/packages/lib/constants/date-formats.ts @@ -17,6 +17,8 @@ export const VALID_DATE_FORMAT_VALUES = [ "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", ] as const; +export type ValidDateFormat = (typeof VALID_DATE_FORMAT_VALUES)[number]; + export const DATE_FORMATS = [ { key: 'yyyy-MM-dd_hh:mm_a', @@ -94,3 +96,7 @@ export const convertToLocalSystemFormat = ( return formattedDate; }; + +export const isValidDateFormat = (dateFormat: unknown): dateFormat is ValidDateFormat => { + return VALID_DATE_FORMAT_VALUES.includes(dateFormat as ValidDateFormat); +}; diff --git a/packages/lib/jobs/definitions/emails/send-team-deleted-email.ts b/packages/lib/jobs/definitions/emails/send-team-deleted-email.ts index 03c32b552..1475a531e 100644 --- a/packages/lib/jobs/definitions/emails/send-team-deleted-email.ts +++ b/packages/lib/jobs/definitions/emails/send-team-deleted-email.ts @@ -25,6 +25,7 @@ const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({ typedSignatureEnabled: z.boolean(), uploadSignatureEnabled: z.boolean(), drawSignatureEnabled: z.boolean(), + allowEmbeddedAuthoring: z.boolean(), }) .nullish(), }), diff --git a/packages/lib/server-only/recipient/set-document-recipients.ts b/packages/lib/server-only/recipient/set-document-recipients.ts index d7d9d0289..33a14a28f 100644 --- a/packages/lib/server-only/recipient/set-document-recipients.ts +++ b/packages/lib/server-only/recipient/set-document-recipients.ts @@ -252,7 +252,10 @@ export const setDocumentRecipients = async ({ }); } - return upsertedRecipient; + return { + ...upsertedRecipient, + clientId: recipient.clientId, + }; }), ); }); @@ -332,7 +335,7 @@ export const setDocumentRecipients = async ({ } // Filter out recipients that have been removed or have been updated. - const filteredRecipients: Recipient[] = existingRecipients.filter((recipient) => { + const filteredRecipients: RecipientDataWithClientId[] = existingRecipients.filter((recipient) => { const isRemoved = removedRecipients.find( (removedRecipient) => removedRecipient.id === recipient.id, ); @@ -353,6 +356,7 @@ export const setDocumentRecipients = async ({ */ type RecipientData = { id?: number | null; + clientId?: string | null; email: string; name: string; role: RecipientRole; @@ -361,6 +365,10 @@ type RecipientData = { actionAuth?: TRecipientActionAuthTypes | null; }; +type RecipientDataWithClientId = Recipient & { + clientId?: string | null; +}; + const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => { const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions); diff --git a/packages/prisma/migrations/20250501123616_add_embedded_authoring_team_flag/migration.sql b/packages/prisma/migrations/20250501123616_add_embedded_authoring_team_flag/migration.sql new file mode 100644 index 000000000..be802e907 --- /dev/null +++ b/packages/prisma/migrations/20250501123616_add_embedded_authoring_team_flag/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "TeamGlobalSettings" ADD COLUMN "allowEmbeddedAuthoring" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 0ba161545..c5b0069b0 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -565,6 +565,8 @@ model TeamGlobalSettings { brandingCompanyDetails String @default("") brandingHidePoweredBy Boolean @default(false) + allowEmbeddedAuthoring Boolean @default(false) + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) } diff --git a/packages/trpc/server/embedding-router/_router.ts b/packages/trpc/server/embedding-router/_router.ts index ca65e0418..030132f99 100644 --- a/packages/trpc/server/embedding-router/_router.ts +++ b/packages/trpc/server/embedding-router/_router.ts @@ -2,7 +2,8 @@ import { router } from '../trpc'; import { createEmbeddingDocumentRoute } from './create-embedding-document'; import { createEmbeddingPresignTokenRoute } from './create-embedding-presign-token'; import { createEmbeddingTemplateRoute } from './create-embedding-template'; -import { getEmbeddingDocumentRoute } from './get-embedding-document'; +import { updateEmbeddingDocumentRoute } from './update-embedding-document'; +import { updateEmbeddingTemplateRoute } from './update-embedding-template'; import { verifyEmbeddingPresignTokenRoute } from './verify-embedding-presign-token'; export const embeddingPresignRouter = router({ @@ -10,5 +11,6 @@ export const embeddingPresignRouter = router({ verifyEmbeddingPresignToken: verifyEmbeddingPresignTokenRoute, createEmbeddingDocument: createEmbeddingDocumentRoute, createEmbeddingTemplate: createEmbeddingTemplateRoute, - getEmbeddingDocument: getEmbeddingDocumentRoute, + updateEmbeddingDocument: updateEmbeddingDocumentRoute, + updateEmbeddingTemplate: updateEmbeddingTemplateRoute, }); diff --git a/packages/trpc/server/embedding-router/create-embedding-presign-token.ts b/packages/trpc/server/embedding-router/create-embedding-presign-token.ts index 819581286..f0c81dacf 100644 --- a/packages/trpc/server/embedding-router/create-embedding-presign-token.ts +++ b/packages/trpc/server/embedding-router/create-embedding-presign-token.ts @@ -1,10 +1,10 @@ import { isCommunityPlan } from '@documenso/ee/server-only/util/is-community-plan'; import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise'; -import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform'; import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { createEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/create-embedding-presign-token'; import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token'; +import { prisma } from '@documenso/prisma'; import { procedure } from '../trpc'; import { @@ -42,13 +42,24 @@ export const createEmbeddingPresignTokenRoute = procedure }); } - const [hasCommunityPlan, hasPlatformPlan, hasEnterprisePlan] = await Promise.all([ + const [hasCommunityPlan, hasEnterprisePlan] = await Promise.all([ isCommunityPlan({ userId: token.userId, teamId: token.teamId ?? undefined }), - isDocumentPlatform({ userId: token.userId, teamId: token.teamId }), isUserEnterprise({ userId: token.userId, teamId: token.teamId ?? undefined }), ]); - if (!hasCommunityPlan && !hasPlatformPlan && !hasEnterprisePlan) { + let hasTeamAuthoringFlag = false; + + if (token.teamId) { + const teamGlobalSettings = await prisma.teamGlobalSettings.findFirst({ + where: { + teamId: token.teamId, + }, + }); + + hasTeamAuthoringFlag = teamGlobalSettings?.allowEmbeddedAuthoring ?? false; + } + + if (!hasCommunityPlan && !hasEnterprisePlan && !hasTeamAuthoringFlag) { throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'You do not have permission to create embedding presign tokens', }); diff --git a/packages/trpc/server/embedding-router/get-embedding-document.ts b/packages/trpc/server/embedding-router/get-embedding-document.ts deleted file mode 100644 index cfa3b7ba2..000000000 --- a/packages/trpc/server/embedding-router/get-embedding-document.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; -import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token'; -import { prisma } from '@documenso/prisma'; - -import { procedure } from '../trpc'; -import { - ZGetEmbeddingDocumentRequestSchema, - ZGetEmbeddingDocumentResponseSchema, -} from './get-embedding-document.types'; - -export const getEmbeddingDocumentRoute = procedure - .input(ZGetEmbeddingDocumentRequestSchema) - .output(ZGetEmbeddingDocumentResponseSchema) - .query(async ({ input, ctx: { req } }) => { - try { - const authorizationHeader = req.headers.get('authorization'); - - const [presignToken] = (authorizationHeader || '') - .split('Bearer ') - .filter((s) => s.length > 0); - - if (!presignToken) { - throw new AppError(AppErrorCode.UNAUTHORIZED, { - message: 'No presign token provided', - }); - } - - const apiToken = await verifyEmbeddingPresignToken({ token: presignToken }); - - const { documentId } = input; - - const document = await prisma.document.findFirst({ - where: { - id: documentId, - userId: apiToken.userId, - ...(apiToken.teamId ? { teamId: apiToken.teamId } : {}), - }, - include: { - documentData: true, - recipients: true, - fields: true, - }, - }); - - if (!document) { - throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'Document not found', - }); - } - - return { - document, - }; - } catch (error) { - if (error instanceof AppError) { - throw error; - } - - throw new AppError(AppErrorCode.UNKNOWN_ERROR, { - message: 'Failed to get document', - }); - } - }); diff --git a/packages/trpc/server/embedding-router/get-embedding-document.types.ts b/packages/trpc/server/embedding-router/get-embedding-document.types.ts deleted file mode 100644 index 58f7d4a2c..000000000 --- a/packages/trpc/server/embedding-router/get-embedding-document.types.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { DocumentDataType, type Field, type Recipient } from '@prisma/client'; -import { z } from 'zod'; - -export const ZGetEmbeddingDocumentRequestSchema = z.object({ - documentId: z.number(), -}); - -export const ZGetEmbeddingDocumentResponseSchema = z.object({ - document: z - .object({ - id: z.number(), - title: z.string(), - status: z.string(), - documentDataId: z.string(), - userId: z.number(), - teamId: z.number().nullable(), - createdAt: z.date(), - updatedAt: z.date(), - documentData: z.object({ - id: z.string(), - type: z.nativeEnum(DocumentDataType), - data: z.string(), - initialData: z.string(), - }), - recipients: z.array(z.custom()), - fields: z.array(z.custom()), - }) - .nullable(), -}); - -export type TGetEmbeddingDocumentRequestSchema = z.infer; -export type TGetEmbeddingDocumentResponseSchema = z.infer< - typeof ZGetEmbeddingDocumentResponseSchema ->; diff --git a/packages/trpc/server/embedding-router/update-embedding-document.ts b/packages/trpc/server/embedding-router/update-embedding-document.ts new file mode 100644 index 000000000..9060b09ec --- /dev/null +++ b/packages/trpc/server/embedding-router/update-embedding-document.ts @@ -0,0 +1,118 @@ +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta'; +import { updateDocument } from '@documenso/lib/server-only/document/update-document'; +import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token'; +import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document'; +import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients'; +import { nanoid } from '@documenso/lib/universal/id'; + +import { procedure } from '../trpc'; +import { + ZUpdateEmbeddingDocumentRequestSchema, + ZUpdateEmbeddingDocumentResponseSchema, +} from './update-embedding-document.types'; + +export const updateEmbeddingDocumentRoute = procedure + .input(ZUpdateEmbeddingDocumentRequestSchema) + .output(ZUpdateEmbeddingDocumentResponseSchema) + .mutation(async ({ input, ctx }) => { + try { + const authorizationHeader = ctx.req.headers.get('authorization'); + + const [presignToken] = (authorizationHeader || '') + .split('Bearer ') + .filter((s) => s.length > 0); + + if (!presignToken) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'No presign token provided', + }); + } + + const apiToken = await verifyEmbeddingPresignToken({ token: presignToken }); + + const { documentId, title, externalId, recipients, meta } = input; + + if (meta && Object.values(meta).length > 0) { + await upsertDocumentMeta({ + documentId: documentId, + userId: apiToken.userId, + teamId: apiToken.teamId ?? undefined, + ...meta, + requestMetadata: ctx.metadata, + }); + } + + await updateDocument({ + userId: apiToken.userId, + teamId: apiToken.teamId ?? undefined, + documentId: documentId, + data: { + title, + externalId, + }, + requestMetadata: ctx.metadata, + }); + + const recipientsWithClientId = recipients.map((recipient) => ({ + ...recipient, + clientId: nanoid(), + })); + + const { recipients: updatedRecipients } = await setDocumentRecipients({ + userId: apiToken.userId, + teamId: apiToken.teamId ?? undefined, + documentId: documentId, + recipients: recipientsWithClientId.map((recipient) => ({ + id: recipient.id, + clientId: recipient.clientId, + email: recipient.email, + name: recipient.name ?? '', + role: recipient.role, + signingOrder: recipient.signingOrder, + })), + requestMetadata: ctx.metadata, + }); + + const fields = recipientsWithClientId.flatMap((recipient) => { + const recipientId = updatedRecipients.find((r) => r.clientId === recipient.clientId)?.id; + + if (!recipientId) { + throw new AppError(AppErrorCode.UNKNOWN_ERROR, { + message: 'Recipient not found', + }); + } + + return (recipient.fields ?? []).map((field) => ({ + ...field, + recipientId, + // !: Temp property to be removed once we don't link based on signer email + signerEmail: recipient.email, + })); + }); + + await setFieldsForDocument({ + userId: apiToken.userId, + teamId: apiToken.teamId ?? undefined, + documentId, + fields: fields.map((field) => ({ + ...field, + pageWidth: field.width, + pageHeight: field.height, + })), + requestMetadata: ctx.metadata, + }); + + return { + documentId, + }; + } catch (error) { + if (error instanceof AppError) { + throw error; + } + + throw new AppError(AppErrorCode.UNKNOWN_ERROR, { + message: 'Failed to update document', + }); + } + }); diff --git a/packages/trpc/server/embedding-router/update-embedding-document.types.ts b/packages/trpc/server/embedding-router/update-embedding-document.types.ts new file mode 100644 index 000000000..183ede703 --- /dev/null +++ b/packages/trpc/server/embedding-router/update-embedding-document.types.ts @@ -0,0 +1,87 @@ +import { z } from 'zod'; + +import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; +import { + ZFieldHeightSchema, + ZFieldPageNumberSchema, + ZFieldPageXSchema, + ZFieldPageYSchema, + ZFieldWidthSchema, +} from '@documenso/lib/types/field'; +import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta'; +import { DocumentSigningOrder, RecipientRole } from '@documenso/prisma/generated/types'; + +import { + ZDocumentExternalIdSchema, + ZDocumentMetaDateFormatSchema, + ZDocumentMetaDistributionMethodSchema, + ZDocumentMetaDrawSignatureEnabledSchema, + ZDocumentMetaLanguageSchema, + ZDocumentMetaMessageSchema, + ZDocumentMetaRedirectUrlSchema, + ZDocumentMetaSubjectSchema, + ZDocumentMetaTimezoneSchema, + ZDocumentMetaTypedSignatureEnabledSchema, + ZDocumentMetaUploadSignatureEnabledSchema, + ZDocumentTitleSchema, +} from '../document-router/schema'; + +export const ZUpdateEmbeddingDocumentRequestSchema = z.object({ + documentId: z.number(), + title: ZDocumentTitleSchema, + externalId: ZDocumentExternalIdSchema.optional(), + recipients: z + .array( + z.object({ + id: z.number().optional(), + email: z.string().toLowerCase().email().min(1), + name: z.string(), + role: z.nativeEnum(RecipientRole), + signingOrder: z.number().optional(), + fields: ZFieldAndMetaSchema.and( + z.object({ + id: z.number().optional(), + pageNumber: ZFieldPageNumberSchema, + pageX: ZFieldPageXSchema, + pageY: ZFieldPageYSchema, + width: ZFieldWidthSchema, + height: ZFieldHeightSchema, + }), + ) + .array() + .optional(), + }), + ) + .refine( + (recipients) => { + const emails = recipients.map((recipient) => recipient.email); + + return new Set(emails).size === emails.length; + }, + { message: 'Recipients must have unique emails' }, + ), + meta: z + .object({ + subject: ZDocumentMetaSubjectSchema.optional(), + message: ZDocumentMetaMessageSchema.optional(), + timezone: ZDocumentMetaTimezoneSchema.optional(), + dateFormat: ZDocumentMetaDateFormatSchema.optional(), + distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(), + signingOrder: z.nativeEnum(DocumentSigningOrder).optional(), + redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(), + language: ZDocumentMetaLanguageSchema.optional(), + typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(), + drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(), + uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(), + emailSettings: ZDocumentEmailSettingsSchema.optional(), + }) + .optional(), +}); + +export const ZUpdateEmbeddingDocumentResponseSchema = z.object({ + documentId: z.number(), +}); + +export type TUpdateEmbeddingDocumentRequestSchema = z.infer< + typeof ZUpdateEmbeddingDocumentRequestSchema +>; diff --git a/packages/trpc/server/embedding-router/update-embedding-template.ts b/packages/trpc/server/embedding-router/update-embedding-template.ts new file mode 100644 index 000000000..52dca3004 --- /dev/null +++ b/packages/trpc/server/embedding-router/update-embedding-template.ts @@ -0,0 +1,104 @@ +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token'; +import { setFieldsForTemplate } from '@documenso/lib/server-only/field/set-fields-for-template'; +import { setTemplateRecipients } from '@documenso/lib/server-only/recipient/set-template-recipients'; +import { updateTemplate } from '@documenso/lib/server-only/template/update-template'; +import { nanoid } from '@documenso/lib/universal/id'; + +import { procedure } from '../trpc'; +import { + ZUpdateEmbeddingTemplateRequestSchema, + ZUpdateEmbeddingTemplateResponseSchema, +} from './update-embedding-template.types'; + +export const updateEmbeddingTemplateRoute = procedure + .input(ZUpdateEmbeddingTemplateRequestSchema) + .output(ZUpdateEmbeddingTemplateResponseSchema) + .mutation(async ({ input, ctx }) => { + try { + const authorizationHeader = ctx.req.headers.get('authorization'); + + const [presignToken] = (authorizationHeader || '') + .split('Bearer ') + .filter((s) => s.length > 0); + + if (!presignToken) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'No presign token provided', + }); + } + + const apiToken = await verifyEmbeddingPresignToken({ token: presignToken }); + + const { templateId, title, externalId, recipients, meta } = input; + + await updateTemplate({ + templateId, + userId: apiToken.userId, + teamId: apiToken.teamId ?? undefined, + data: { + title, + externalId, + }, + meta, + }); + + const recipientsWithClientId = recipients.map((recipient) => ({ + ...recipient, + clientId: nanoid(), + })); + + const { recipients: updatedRecipients } = await setTemplateRecipients({ + userId: apiToken.userId, + teamId: apiToken.teamId ?? undefined, + templateId, + recipients: recipientsWithClientId.map((recipient) => ({ + id: recipient.id, + email: recipient.email, + name: recipient.name ?? '', + role: recipient.role ?? 'SIGNER', + signingOrder: recipient.signingOrder, + })), + }); + + const fields = recipientsWithClientId.flatMap((recipient) => { + const recipientId = updatedRecipients.find((r) => r.email === recipient.email)?.id; + + if (!recipientId) { + throw new AppError(AppErrorCode.UNKNOWN_ERROR, { + message: 'Recipient not found', + }); + } + + return (recipient.fields ?? []).map((field) => ({ + ...field, + recipientId, + // !: Temp property to be removed once we don't link based on signer email + signerEmail: recipient.email, + })); + }); + + await setFieldsForTemplate({ + userId: apiToken.userId, + teamId: apiToken.teamId ?? undefined, + templateId, + fields: fields.map((field) => ({ + ...field, + pageWidth: field.width, + pageHeight: field.height, + })), + }); + + return { + templateId, + }; + } catch (error) { + if (error instanceof AppError) { + throw error; + } + + throw new AppError(AppErrorCode.UNKNOWN_ERROR, { + message: 'Failed to update template', + }); + } + }); diff --git a/packages/trpc/server/embedding-router/update-embedding-template.types.ts b/packages/trpc/server/embedding-router/update-embedding-template.types.ts new file mode 100644 index 000000000..c25ea0a65 --- /dev/null +++ b/packages/trpc/server/embedding-router/update-embedding-template.types.ts @@ -0,0 +1,77 @@ +import { DocumentSigningOrder, FieldType, RecipientRole } from '@prisma/client'; +import { z } from 'zod'; + +import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; +import { + ZFieldHeightSchema, + ZFieldPageNumberSchema, + ZFieldPageXSchema, + ZFieldPageYSchema, + ZFieldWidthSchema, +} from '@documenso/lib/types/field'; +import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta'; + +import { + ZDocumentMetaDateFormatSchema, + ZDocumentMetaDistributionMethodSchema, + ZDocumentMetaDrawSignatureEnabledSchema, + ZDocumentMetaLanguageSchema, + ZDocumentMetaMessageSchema, + ZDocumentMetaRedirectUrlSchema, + ZDocumentMetaSubjectSchema, + ZDocumentMetaTimezoneSchema, + ZDocumentMetaTypedSignatureEnabledSchema, + ZDocumentMetaUploadSignatureEnabledSchema, + ZDocumentTitleSchema, +} from '../document-router/schema'; + +const ZFieldSchema = z.object({ + id: z.number().optional(), + type: z.nativeEnum(FieldType), + pageNumber: ZFieldPageNumberSchema, + pageX: ZFieldPageXSchema, + pageY: ZFieldPageYSchema, + width: ZFieldWidthSchema, + height: ZFieldHeightSchema, + fieldMeta: ZFieldMetaSchema.optional(), +}); + +export const ZUpdateEmbeddingTemplateRequestSchema = z.object({ + templateId: z.number(), + title: ZDocumentTitleSchema.optional(), + externalId: z.string().optional(), + recipients: z.array( + z.object({ + id: z.number().optional(), + email: z.string().email(), + name: z.string().optional(), + role: z.nativeEnum(RecipientRole).optional(), + signingOrder: z.number().optional(), + fields: z.array(ZFieldSchema).optional(), + }), + ), + meta: z + .object({ + subject: ZDocumentMetaSubjectSchema.optional(), + message: ZDocumentMetaMessageSchema.optional(), + timezone: ZDocumentMetaTimezoneSchema.optional(), + dateFormat: ZDocumentMetaDateFormatSchema.optional(), + distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(), + signingOrder: z.nativeEnum(DocumentSigningOrder).optional(), + redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(), + language: ZDocumentMetaLanguageSchema.optional(), + typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(), + drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(), + uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(), + emailSettings: ZDocumentEmailSettingsSchema.optional(), + }) + .optional(), +}); + +export const ZUpdateEmbeddingTemplateResponseSchema = z.object({ + templateId: z.number(), +}); + +export type TUpdateEmbeddingTemplateRequestSchema = z.infer< + typeof ZUpdateEmbeddingTemplateRequestSchema +>; diff --git a/packages/trpc/server/recipient-router/schema.ts b/packages/trpc/server/recipient-router/schema.ts index 149120f87..0c43838a2 100644 --- a/packages/trpc/server/recipient-router/schema.ts +++ b/packages/trpc/server/recipient-router/schema.ts @@ -30,7 +30,7 @@ export const ZCreateRecipientSchema = z.object({ actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(), }); -const ZUpdateRecipientSchema = z.object({ +export const ZUpdateRecipientSchema = z.object({ id: z.number().describe('The ID of the recipient to update.'), email: z.string().toLowerCase().email().min(1).optional(), name: z.string().optional(), diff --git a/packages/ui/primitives/recipient-selector.tsx b/packages/ui/primitives/recipient-selector.tsx index d47ba89ef..c4aba87ac 100644 --- a/packages/ui/primitives/recipient-selector.tsx +++ b/packages/ui/primitives/recipient-selector.tsx @@ -3,7 +3,7 @@ import { useCallback, useState } from 'react'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import type { Recipient } from '@prisma/client'; -import { RecipientRole, SendStatus } from '@prisma/client'; +import { RecipientRole, SendStatus, SigningStatus } from '@prisma/client'; import { Check, ChevronsUpDown, Info } from 'lucide-react'; import { sortBy } from 'remeda'; @@ -145,6 +145,7 @@ export const RecipientSelector = ({ onSelectedRecipientChange(recipient); setShowRecipientsSelector(false); }} + disabled={recipient.signingStatus !== SigningStatus.NOT_SIGNED} > Date: Thu, 1 May 2025 19:46:59 +0300 Subject: [PATCH 04/12] feat: add folders (#1711) --- .../document-move-to-folder-dialog.tsx | 216 ++ .../dialogs/folder-create-dialog.tsx | 163 + .../dialogs/folder-delete-dialog.tsx | 159 + .../components/dialogs/folder-move-dialog.tsx | 169 + .../dialogs/folder-settings-dialog.tsx | 173 + .../dialogs/template-create-dialog.tsx | 5 +- .../dialogs/template-folder-create-dialog.tsx | 164 + .../dialogs/template-folder-delete-dialog.tsx | 163 + .../dialogs/template-folder-move-dialog.tsx | 175 + .../template-folder-settings-dialog.tsx | 176 + .../template-move-to-folder-dialog.tsx | 213 ++ .../document/document-drop-zone-wrapper.tsx | 192 ++ .../document/document-page-view-button.tsx | 5 +- .../general/document/document-upload.tsx | 62 +- .../components/general/folder/folder-card.tsx | 88 + .../general/template/template-edit-form.tsx | 6 +- .../tables/documents-table-action-button.tsx | 5 +- .../documents-table-action-dropdown.tsx | 18 +- .../app/components/tables/documents-table.tsx | 20 +- .../templates-table-action-dropdown.tsx | 36 +- .../app/components/tables/templates-table.tsx | 5 + .../admin+/documents._index.tsx | 4 +- .../_authenticated+/documents.$id._index.tsx | 4 + .../_authenticated+/documents.$id.edit.tsx | 9 +- .../_authenticated+/documents.$id.logs.tsx | 28 +- .../_authenticated+/documents._index.tsx | 350 +- .../documents.f.$folderId.$id._index.tsx | 272 ++ .../documents.f.$folderId.$id.edit.tsx | 155 + .../documents.f.$folderId.$id.logs.tsx | 199 ++ .../documents.f.$folderId._index.tsx | 374 +++ .../documents.folders._index.tsx | 181 ++ .../documents.f.$folderId.$id._index.tsx | 5 + .../documents.f.$folderId.$id.edit.tsx | 5 + .../documents.f.$folderId.$id.logs.tsx | 5 + .../documents.f.$folderId._index.tsx | 5 + .../t.$teamUrl+/documents.folders._index.tsx | 5 + .../templates.f.$folderId.$id._index.tsx | 5 + .../templates.f.$folderId.$id.edit.tsx | 5 + .../templates.f.$folderId._index.tsx | 5 + .../t.$teamUrl+/templates.folders._index.tsx | 5 + .../_authenticated+/templates.$id._index.tsx | 4 + .../_authenticated+/templates.$id.edit.tsx | 15 +- .../_authenticated+/templates._index.tsx | 264 +- .../templates.f.$folderId.$id._index.tsx | 230 ++ .../templates.f.$folderId.$id.edit.tsx | 122 + .../templates.f.$folderId._index.tsx | 369 +++ .../templates.folders._index.tsx | 181 ++ .../routes/_recipient+/d.$token+/_index.tsx | 5 +- .../document-flow/stepper-component.spec.ts | 15 +- .../individual-account-folders.spec.ts | 842 +++++ .../e2e/folders/team-account-folders.spec.ts | 2873 +++++++++++++++++ packages/app-tests/package.json | 2 +- .../document/create-document-v2.ts | 1 + .../server-only/document/create-document.ts | 38 +- .../server-only/document/find-documents.ts | 37 +- .../document/get-document-by-id.ts | 13 +- .../get-document-with-details-by-id.ts | 8 +- .../lib/server-only/document/get-stats.ts | 29 +- .../lib/server-only/folder/create-folder.ts | 86 + .../lib/server-only/folder/delete-folder.ts | 85 + .../lib/server-only/folder/find-folders.ts | 152 + .../folder/get-folder-breadcrumbs.ts | 112 + .../server-only/folder/get-folder-by-id.ts | 92 + .../folder/move-document-to-folder.ts | 130 + .../lib/server-only/folder/move-folder.ts | 85 + .../folder/move-template-to-folder.ts | 59 + packages/lib/server-only/folder/pin-folder.ts | 37 + .../lib/server-only/folder/unpin-folder.ts | 37 + .../lib/server-only/folder/update-folder.ts | 53 + .../server-only/template/create-template.ts | 27 + .../server-only/template/find-templates.ts | 8 + .../template/get-template-by-id.ts | 10 +- packages/lib/types/document.ts | 16 + packages/lib/types/folder-type.ts | 9 + packages/lib/types/template.ts | 16 + packages/lib/utils/format-folder-count.ts | 5 + .../migration.sql | 39 + .../migration.sql | 29 + .../migration.sql | 2 + .../migration.sql | 20 + .../migration.sql | 11 + packages/prisma/schema.prisma | 36 + packages/prisma/seed/folders.ts | 33 + .../trpc/server/document-router/router.ts | 24 +- .../trpc/server/document-router/schema.ts | 4 + packages/trpc/server/folder-router/router.ts | 354 ++ packages/trpc/server/folder-router/schema.ts | 132 + packages/trpc/server/router.ts | 2 + .../trpc/server/template-router/router.ts | 3 +- .../trpc/server/template-router/schema.ts | 2 + packages/ui/primitives/document-upload.tsx | 88 + 91 files changed, 10497 insertions(+), 183 deletions(-) create mode 100644 apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx create mode 100644 apps/remix/app/components/dialogs/folder-create-dialog.tsx create mode 100644 apps/remix/app/components/dialogs/folder-delete-dialog.tsx create mode 100644 apps/remix/app/components/dialogs/folder-move-dialog.tsx create mode 100644 apps/remix/app/components/dialogs/folder-settings-dialog.tsx create mode 100644 apps/remix/app/components/dialogs/template-folder-create-dialog.tsx create mode 100644 apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx create mode 100644 apps/remix/app/components/dialogs/template-folder-move-dialog.tsx create mode 100644 apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx create mode 100644 apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx create mode 100644 apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx create mode 100644 apps/remix/app/components/general/folder/folder-card.tsx create mode 100644 apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx create mode 100644 apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.edit.tsx create mode 100644 apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx create mode 100644 apps/remix/app/routes/_authenticated+/documents.f.$folderId._index.tsx create mode 100644 apps/remix/app/routes/_authenticated+/documents.folders._index.tsx create mode 100644 apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId.$id._index.tsx create mode 100644 apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId.$id.edit.tsx create mode 100644 apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId.$id.logs.tsx create mode 100644 apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId._index.tsx create mode 100644 apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx create mode 100644 apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId.$id._index.tsx create mode 100644 apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId.$id.edit.tsx create mode 100644 apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx create mode 100644 apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx create mode 100644 apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx create mode 100644 apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id.edit.tsx create mode 100644 apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx create mode 100644 apps/remix/app/routes/_authenticated+/templates.folders._index.tsx create mode 100644 packages/app-tests/e2e/folders/individual-account-folders.spec.ts create mode 100644 packages/app-tests/e2e/folders/team-account-folders.spec.ts create mode 100644 packages/lib/server-only/folder/create-folder.ts create mode 100644 packages/lib/server-only/folder/delete-folder.ts create mode 100644 packages/lib/server-only/folder/find-folders.ts create mode 100644 packages/lib/server-only/folder/get-folder-breadcrumbs.ts create mode 100644 packages/lib/server-only/folder/get-folder-by-id.ts create mode 100644 packages/lib/server-only/folder/move-document-to-folder.ts create mode 100644 packages/lib/server-only/folder/move-folder.ts create mode 100644 packages/lib/server-only/folder/move-template-to-folder.ts create mode 100644 packages/lib/server-only/folder/pin-folder.ts create mode 100644 packages/lib/server-only/folder/unpin-folder.ts create mode 100644 packages/lib/server-only/folder/update-folder.ts create mode 100644 packages/lib/types/folder-type.ts create mode 100644 packages/lib/utils/format-folder-count.ts create mode 100644 packages/prisma/migrations/20250318072444_add_document_folders/migration.sql create mode 100644 packages/prisma/migrations/20250324135010_model_folder_update_id_type_and_add_pinned_property/migration.sql create mode 100644 packages/prisma/migrations/20250328135807_add_folder_visibility/migration.sql create mode 100644 packages/prisma/migrations/20250404111643_add_folder_type_and_template_folders/migration.sql create mode 100644 packages/prisma/migrations/20250407124054_add_delete_oncascade_folder/migration.sql create mode 100644 packages/prisma/seed/folders.ts create mode 100644 packages/trpc/server/folder-router/router.ts create mode 100644 packages/trpc/server/folder-router/schema.ts create mode 100644 packages/ui/primitives/document-upload.tsx diff --git a/apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx b/apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx new file mode 100644 index 000000000..e8aefd0c0 --- /dev/null +++ b/apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx @@ -0,0 +1,216 @@ +import { useEffect } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import type * as DialogPrimitive from '@radix-ui/react-dialog'; +import { FolderIcon, HomeIcon, Loader2 } from 'lucide-react'; +import { useForm } from 'react-hook-form'; +import { useNavigate } from 'react-router'; +import { z } from 'zod'; + +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { FolderType } from '@documenso/lib/types/folder-type'; +import { formatDocumentsPath } from '@documenso/lib/utils/teams'; +import { trpc } from '@documenso/trpc/react'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@documenso/ui/primitives/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +import { useOptionalCurrentTeam } from '~/providers/team'; + +export type DocumentMoveToFolderDialogProps = { + documentId: number; + open: boolean; + onOpenChange: (open: boolean) => void; + currentFolderId?: string; +} & Omit; + +const ZMoveDocumentFormSchema = z.object({ + folderId: z.string().nullable().optional(), +}); + +type TMoveDocumentFormSchema = z.infer; + +export const DocumentMoveToFolderDialog = ({ + documentId, + open, + onOpenChange, + currentFolderId, + ...props +}: DocumentMoveToFolderDialogProps) => { + const { _ } = useLingui(); + const { toast } = useToast(); + const navigate = useNavigate(); + const team = useOptionalCurrentTeam(); + + const form = useForm({ + resolver: zodResolver(ZMoveDocumentFormSchema), + defaultValues: { + folderId: currentFolderId, + }, + }); + + const { data: folders, isLoading: isFoldersLoading } = trpc.folder.findFolders.useQuery( + { + parentId: currentFolderId, + type: FolderType.DOCUMENT, + }, + { + enabled: open, + }, + ); + + const { mutateAsync: moveDocumentToFolder } = trpc.folder.moveDocumentToFolder.useMutation(); + + useEffect(() => { + if (!open) { + form.reset(); + } else { + form.reset({ folderId: currentFolderId }); + } + }, [open, currentFolderId, form]); + + const onSubmit = async (data: TMoveDocumentFormSchema) => { + try { + await moveDocumentToFolder({ + documentId, + folderId: data.folderId ?? null, + }); + + toast({ + title: _(msg`Document moved`), + description: _(msg`The document has been moved successfully.`), + variant: 'default', + }); + + onOpenChange(false); + + const documentsPath = formatDocumentsPath(team?.url); + + if (data.folderId) { + void navigate(`${documentsPath}/f/${data.folderId}`); + } else { + void navigate(documentsPath); + } + } catch (err) { + const error = AppError.parseError(err); + + if (error.code === AppErrorCode.NOT_FOUND) { + toast({ + title: _(msg`Error`), + description: _(msg`The folder you are trying to move the document to does not exist.`), + variant: 'destructive', + }); + + return; + } + + toast({ + title: _(msg`Error`), + description: _(msg`An error occurred while moving the document.`), + variant: 'destructive', + }); + } + }; + + return ( + + + + + Move Document to Folder + + + + Select a folder to move this document to. + + + + + + ( + + + Folder + + +
+ {isFoldersLoading ? ( +
+ +
+ ) : ( + <> + + + {folders?.data.map((folder) => ( + + ))} + + )} +
+
+ +
+ )} + /> + + + + + + + + +
+
+ ); +}; diff --git a/apps/remix/app/components/dialogs/folder-create-dialog.tsx b/apps/remix/app/components/dialogs/folder-create-dialog.tsx new file mode 100644 index 000000000..c0544ba3a --- /dev/null +++ b/apps/remix/app/components/dialogs/folder-create-dialog.tsx @@ -0,0 +1,163 @@ +import { useEffect, useState } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import type * as DialogPrimitive from '@radix-ui/react-dialog'; +import { FolderPlusIcon } from 'lucide-react'; +import { useForm } from 'react-hook-form'; +import { useNavigate, useParams } from 'react-router'; +import { z } from 'zod'; + +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { FolderType } from '@documenso/lib/types/folder-type'; +import { formatDocumentsPath } from '@documenso/lib/utils/teams'; +import { trpc } from '@documenso/trpc/react'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { Input } from '@documenso/ui/primitives/input'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +import { useOptionalCurrentTeam } from '~/providers/team'; + +const ZCreateFolderFormSchema = z.object({ + name: z.string().min(1, { message: 'Folder name is required' }), +}); + +type TCreateFolderFormSchema = z.infer; + +export type CreateFolderDialogProps = { + trigger?: React.ReactNode; +} & Omit; + +export const CreateFolderDialog = ({ trigger, ...props }: CreateFolderDialogProps) => { + const { _ } = useLingui(); + const { toast } = useToast(); + const { folderId } = useParams(); + + const navigate = useNavigate(); + const team = useOptionalCurrentTeam(); + + const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false); + + const { mutateAsync: createFolder } = trpc.folder.createFolder.useMutation(); + + const form = useForm({ + resolver: zodResolver(ZCreateFolderFormSchema), + defaultValues: { + name: '', + }, + }); + + const onSubmit = async (data: TCreateFolderFormSchema) => { + try { + const newFolder = await createFolder({ + name: data.name, + parentId: folderId, + type: FolderType.DOCUMENT, + }); + + setIsCreateFolderOpen(false); + + toast({ + description: 'Folder created successfully', + }); + + const documentsPath = formatDocumentsPath(team?.url); + + void navigate(`${documentsPath}/f/${newFolder.id}`); + } catch (err) { + const error = AppError.parseError(err); + + if (error.code === AppErrorCode.ALREADY_EXISTS) { + toast({ + title: 'Failed to create folder', + description: _(msg`This folder name is already taken.`), + variant: 'destructive', + }); + + return; + } + + toast({ + title: 'Failed to create folder', + description: _(msg`An unknown error occurred while creating the folder.`), + variant: 'destructive', + }); + } + }; + + useEffect(() => { + if (!isCreateFolderOpen) { + form.reset(); + } + }, [isCreateFolderOpen, form]); + + return ( + + + {trigger ?? ( + + )} + + + + + Create New Folder + + Enter a name for your new folder. Folders help you organize your documents. + + + +
+ + ( + + Folder Name + + + + + + )} + /> + + + + + + + + +
+
+ ); +}; diff --git a/apps/remix/app/components/dialogs/folder-delete-dialog.tsx b/apps/remix/app/components/dialogs/folder-delete-dialog.tsx new file mode 100644 index 000000000..e1fd6fac0 --- /dev/null +++ b/apps/remix/app/components/dialogs/folder-delete-dialog.tsx @@ -0,0 +1,159 @@ +import { useEffect } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import type * as DialogPrimitive from '@radix-ui/react-dialog'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { trpc } from '@documenso/trpc/react'; +import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@documenso/ui/primitives/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { Input } from '@documenso/ui/primitives/input'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +export type FolderDeleteDialogProps = { + folder: TFolderWithSubfolders | null; + isOpen: boolean; + onOpenChange: (open: boolean) => void; +} & Omit; + +export const FolderDeleteDialog = ({ folder, isOpen, onOpenChange }: FolderDeleteDialogProps) => { + const { _ } = useLingui(); + + const { toast } = useToast(); + const { mutateAsync: deleteFolder } = trpc.folder.deleteFolder.useMutation(); + + const deleteMessage = _(msg`delete ${folder?.name ?? 'folder'}`); + + const ZDeleteFolderFormSchema = z.object({ + confirmText: z.literal(deleteMessage, { + errorMap: () => ({ message: _(msg`You must type '${deleteMessage}' to confirm`) }), + }), + }); + + type TDeleteFolderFormSchema = z.infer; + + const form = useForm({ + resolver: zodResolver(ZDeleteFolderFormSchema), + defaultValues: { + confirmText: '', + }, + }); + + const onFormSubmit = async () => { + if (!folder) return; + + try { + await deleteFolder({ + id: folder.id, + }); + + onOpenChange(false); + + toast({ + title: 'Folder deleted successfully', + }); + } catch (err) { + const error = AppError.parseError(err); + + if (error.code === AppErrorCode.NOT_FOUND) { + toast({ + title: 'Folder not found', + description: _(msg`The folder you are trying to delete does not exist.`), + variant: 'destructive', + }); + + return; + } + + toast({ + title: 'Failed to delete folder', + description: _(msg`An unknown error occurred while deleting the folder.`), + variant: 'destructive', + }); + } + }; + + useEffect(() => { + if (!isOpen) { + form.reset(); + } + }, [isOpen]); + + return ( + + + + Delete Folder + + Are you sure you want to delete this folder? + {folder && folder._count.documents > 0 && ( + + This folder contains {folder._count.documents} document(s). Deleting it will also + delete all documents in the folder. + + )} + {folder && folder._count.subfolders > 0 && ( + + This folder contains {folder._count.subfolders} subfolder(s). Deleting it will + delete all subfolders and their contents. + + )} + + +
+ + ( + + + + Confirm by typing:{' '} + + {deleteMessage} + + + + + + + + + )} + /> + + + + + + +
+
+ ); +}; diff --git a/apps/remix/app/components/dialogs/folder-move-dialog.tsx b/apps/remix/app/components/dialogs/folder-move-dialog.tsx new file mode 100644 index 000000000..2c12b7c7f --- /dev/null +++ b/apps/remix/app/components/dialogs/folder-move-dialog.tsx @@ -0,0 +1,169 @@ +import { useEffect } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import type * as DialogPrimitive from '@radix-ui/react-dialog'; +import { FolderIcon, HomeIcon } from 'lucide-react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { trpc } from '@documenso/trpc/react'; +import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@documenso/ui/primitives/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +export type FolderMoveDialogProps = { + foldersData: TFolderWithSubfolders[] | undefined; + folder: TFolderWithSubfolders | null; + isOpen: boolean; + onOpenChange: (open: boolean) => void; +} & Omit; + +const ZMoveFolderFormSchema = z.object({ + targetFolderId: z.string().nullable(), +}); + +type TMoveFolderFormSchema = z.infer; + +export const FolderMoveDialog = ({ + foldersData, + folder, + isOpen, + onOpenChange, +}: FolderMoveDialogProps) => { + const { _ } = useLingui(); + + const { toast } = useToast(); + const { mutateAsync: moveFolder } = trpc.folder.moveFolder.useMutation(); + + const form = useForm({ + resolver: zodResolver(ZMoveFolderFormSchema), + defaultValues: { + targetFolderId: folder?.parentId ?? null, + }, + }); + + const onFormSubmit = async ({ targetFolderId }: TMoveFolderFormSchema) => { + if (!folder) return; + + try { + await moveFolder({ + id: folder.id, + parentId: targetFolderId || null, + }); + + onOpenChange(false); + + toast({ + title: 'Folder moved successfully', + }); + } catch (err) { + const error = AppError.parseError(err); + + if (error.code === AppErrorCode.NOT_FOUND) { + toast({ + title: 'Folder not found', + description: _(msg`The folder you are trying to move does not exist.`), + variant: 'destructive', + }); + + return; + } + + toast({ + title: 'Failed to move folder', + description: _(msg`An unknown error occurred while moving the folder.`), + variant: 'destructive', + }); + } + }; + + useEffect(() => { + if (!isOpen) { + form.reset(); + } + }, [isOpen, form]); + + // Filter out the current folder and only show folders of the same type + const filteredFolders = foldersData?.filter( + (f) => f.id !== folder?.id && f.type === folder?.type, + ); + + return ( + + + + Move Folder + Select a destination for this folder. + +
+ + ( + + +
+ + + {filteredFolders && + filteredFolders.map((f) => ( + + ))} +
+
+ +
+ )} + /> + + + + + + +
+
+ ); +}; diff --git a/apps/remix/app/components/dialogs/folder-settings-dialog.tsx b/apps/remix/app/components/dialogs/folder-settings-dialog.tsx new file mode 100644 index 000000000..cae0cd8f4 --- /dev/null +++ b/apps/remix/app/components/dialogs/folder-settings-dialog.tsx @@ -0,0 +1,173 @@ +import { useEffect } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import type * as DialogPrimitive from '@radix-ui/react-dialog'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { DocumentVisibility } from '@documenso/lib/types/document-visibility'; +import { trpc } from '@documenso/trpc/react'; +import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@documenso/ui/primitives/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { Input } from '@documenso/ui/primitives/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@documenso/ui/primitives/select'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +import { useOptionalCurrentTeam } from '~/providers/team'; + +export type FolderSettingsDialogProps = { + folder: TFolderWithSubfolders | null; + isOpen: boolean; + onOpenChange: (open: boolean) => void; +} & Omit; + +export const ZUpdateFolderFormSchema = z.object({ + name: z.string().min(1), + visibility: z.nativeEnum(DocumentVisibility).optional(), +}); + +export type TUpdateFolderFormSchema = z.infer; + +export const FolderSettingsDialog = ({ + folder, + isOpen, + onOpenChange, +}: FolderSettingsDialogProps) => { + const { _ } = useLingui(); + const team = useOptionalCurrentTeam(); + + const { toast } = useToast(); + const { mutateAsync: updateFolder } = trpc.folder.updateFolder.useMutation(); + + const isTeamContext = !!team; + + const form = useForm>({ + resolver: zodResolver(ZUpdateFolderFormSchema), + defaultValues: { + name: folder?.name ?? '', + visibility: folder?.visibility ?? DocumentVisibility.EVERYONE, + }, + }); + + useEffect(() => { + if (folder) { + form.reset({ + name: folder.name, + visibility: folder.visibility ?? DocumentVisibility.EVERYONE, + }); + } + }, [folder, form]); + + const onFormSubmit = async (data: TUpdateFolderFormSchema) => { + if (!folder) return; + + try { + await updateFolder({ + id: folder.id, + name: data.name, + visibility: isTeamContext + ? (data.visibility ?? DocumentVisibility.EVERYONE) + : DocumentVisibility.EVERYONE, + }); + + toast({ + title: _(msg`Folder updated successfully`), + }); + + onOpenChange(false); + } catch (err) { + const error = AppError.parseError(err); + + if (error.code === AppErrorCode.NOT_FOUND) { + toast({ + title: _(msg`Folder not found`), + }); + } + } + }; + + return ( + + + + Folder Settings + Manage the settings for this folder. + + +
+ + ( + + Name + + + + + + )} + /> + + {isTeamContext && ( + ( + + Visibility + + + + )} + /> + )} + + + + + + +
+
+ ); +}; diff --git a/apps/remix/app/components/dialogs/template-create-dialog.tsx b/apps/remix/app/components/dialogs/template-create-dialog.tsx index e623c806b..373f87a21 100644 --- a/apps/remix/app/components/dialogs/template-create-dialog.tsx +++ b/apps/remix/app/components/dialogs/template-create-dialog.tsx @@ -24,11 +24,11 @@ import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone'; import { useToast } from '@documenso/ui/primitives/use-toast'; type TemplateCreateDialogProps = { - teamId?: number; templateRootPath: string; + folderId?: string; }; -export const TemplateCreateDialog = ({ templateRootPath }: TemplateCreateDialogProps) => { +export const TemplateCreateDialog = ({ templateRootPath, folderId }: TemplateCreateDialogProps) => { const navigate = useNavigate(); const { user } = useSession(); @@ -53,6 +53,7 @@ export const TemplateCreateDialog = ({ templateRootPath }: TemplateCreateDialogP const { id } = await createTemplate({ title: file.name, templateDocumentDataId: response.id, + folderId: folderId, }); toast({ diff --git a/apps/remix/app/components/dialogs/template-folder-create-dialog.tsx b/apps/remix/app/components/dialogs/template-folder-create-dialog.tsx new file mode 100644 index 000000000..aaf0322f4 --- /dev/null +++ b/apps/remix/app/components/dialogs/template-folder-create-dialog.tsx @@ -0,0 +1,164 @@ +import { useEffect, useState } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import type * as DialogPrimitive from '@radix-ui/react-dialog'; +import { FolderPlusIcon } from 'lucide-react'; +import { useForm } from 'react-hook-form'; +import { useNavigate, useParams } from 'react-router'; +import { z } from 'zod'; + +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { FolderType } from '@documenso/lib/types/folder-type'; +import { formatTemplatesPath } from '@documenso/lib/utils/teams'; +import { trpc } from '@documenso/trpc/react'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { Input } from '@documenso/ui/primitives/input'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +import { useOptionalCurrentTeam } from '~/providers/team'; + +const ZCreateFolderFormSchema = z.object({ + name: z.string().min(1, { message: 'Folder name is required' }), +}); + +type TCreateFolderFormSchema = z.infer; + +export type TemplateFolderCreateDialogProps = { + trigger?: React.ReactNode; +} & Omit; + +export const TemplateFolderCreateDialog = ({ + trigger, + ...props +}: TemplateFolderCreateDialogProps) => { + const { toast } = useToast(); + const { _ } = useLingui(); + const navigate = useNavigate(); + const team = useOptionalCurrentTeam(); + const { folderId } = useParams(); + + const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false); + + const { mutateAsync: createFolder } = trpc.folder.createFolder.useMutation(); + + const form = useForm({ + resolver: zodResolver(ZCreateFolderFormSchema), + defaultValues: { + name: '', + }, + }); + + const onSubmit = async (data: TCreateFolderFormSchema) => { + try { + const newFolder = await createFolder({ + name: data.name, + parentId: folderId, + type: FolderType.TEMPLATE, + }); + + setIsCreateFolderOpen(false); + + toast({ + description: _(msg`Folder created successfully`), + }); + + const templatesPath = formatTemplatesPath(team?.url); + + void navigate(`${templatesPath}/f/${newFolder.id}`); + } catch (err) { + const error = AppError.parseError(err); + + if (error.code === AppErrorCode.ALREADY_EXISTS) { + toast({ + title: _(msg`Failed to create folder`), + description: _(msg`This folder name is already taken.`), + variant: 'destructive', + }); + + return; + } + + toast({ + title: _(msg`Failed to create folder`), + description: _(msg`An unknown error occurred while creating the folder.`), + variant: 'destructive', + }); + } + }; + + useEffect(() => { + if (!isCreateFolderOpen) { + form.reset(); + } + }, [isCreateFolderOpen, form]); + + return ( + + + {trigger ?? ( + + )} + + + + + Create New Folder + + Enter a name for your new folder. Folders help you organize your templates. + + + +
+ + ( + + Folder Name + + + + + + )} + /> + + + + + + + +
+
+ ); +}; diff --git a/apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx b/apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx new file mode 100644 index 000000000..b337f9c25 --- /dev/null +++ b/apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx @@ -0,0 +1,163 @@ +import { useEffect } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import type * as DialogPrimitive from '@radix-ui/react-dialog'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { trpc } from '@documenso/trpc/react'; +import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@documenso/ui/primitives/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { Input } from '@documenso/ui/primitives/input'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +export type TemplateFolderDeleteDialogProps = { + folder: TFolderWithSubfolders | null; + isOpen: boolean; + onOpenChange: (open: boolean) => void; +} & Omit; + +export const TemplateFolderDeleteDialog = ({ + folder, + isOpen, + onOpenChange, +}: TemplateFolderDeleteDialogProps) => { + const { _ } = useLingui(); + + const { toast } = useToast(); + const { mutateAsync: deleteFolder } = trpc.folder.deleteFolder.useMutation(); + + const deleteMessage = _(msg`delete ${folder?.name ?? 'folder'}`); + + const ZDeleteFolderFormSchema = z.object({ + confirmText: z.literal(deleteMessage, { + errorMap: () => ({ message: _(msg`You must type '${deleteMessage}' to confirm`) }), + }), + }); + + type TDeleteFolderFormSchema = z.infer; + + const form = useForm({ + resolver: zodResolver(ZDeleteFolderFormSchema), + defaultValues: { + confirmText: '', + }, + }); + + const onFormSubmit = async () => { + if (!folder) return; + + try { + await deleteFolder({ + id: folder.id, + }); + + onOpenChange(false); + + toast({ + title: 'Folder deleted successfully', + }); + } catch (err) { + const error = AppError.parseError(err); + + if (error.code === AppErrorCode.NOT_FOUND) { + toast({ + title: 'Folder not found', + description: _(msg`The folder you are trying to delete does not exist.`), + variant: 'destructive', + }); + + return; + } + + toast({ + title: 'Failed to delete folder', + description: _(msg`An unknown error occurred while deleting the folder.`), + variant: 'destructive', + }); + } + }; + + useEffect(() => { + if (!isOpen) { + form.reset(); + } + }, [isOpen]); + + return ( + + + + Delete Folder + + Are you sure you want to delete this folder? + {folder && folder._count.documents > 0 && ( + + This folder contains {folder._count.documents} document(s). Deleting it will also + delete all documents in the folder. + + )} + {folder && folder._count.subfolders > 0 && ( + + This folder contains {folder._count.subfolders} subfolder(s). Deleting it will + delete all subfolders and their contents. + + )} + + +
+ + ( + + + + Confirm by typing:{' '} + + {deleteMessage} + + + + + + + + + )} + /> + + + + + + +
+
+ ); +}; diff --git a/apps/remix/app/components/dialogs/template-folder-move-dialog.tsx b/apps/remix/app/components/dialogs/template-folder-move-dialog.tsx new file mode 100644 index 000000000..e683bceda --- /dev/null +++ b/apps/remix/app/components/dialogs/template-folder-move-dialog.tsx @@ -0,0 +1,175 @@ +import { useEffect } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import type * as DialogPrimitive from '@radix-ui/react-dialog'; +import { FolderIcon, HomeIcon } from 'lucide-react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { trpc } from '@documenso/trpc/react'; +import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@documenso/ui/primitives/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +export type TemplateFolderMoveDialogProps = { + foldersData: TFolderWithSubfolders[] | undefined; + folder: TFolderWithSubfolders | null; + isOpen: boolean; + onOpenChange: (open: boolean) => void; +} & Omit; + +const ZMoveFolderFormSchema = z.object({ + targetFolderId: z.string().optional(), +}); + +type TMoveFolderFormSchema = z.infer; + +export const TemplateFolderMoveDialog = ({ + foldersData, + folder, + isOpen, + onOpenChange, +}: TemplateFolderMoveDialogProps) => { + const { _ } = useLingui(); + + const { toast } = useToast(); + const { mutateAsync: moveFolder } = trpc.folder.moveFolder.useMutation(); + + const form = useForm({ + resolver: zodResolver(ZMoveFolderFormSchema), + defaultValues: { + targetFolderId: folder?.parentId ?? '', + }, + }); + + const onFormSubmit = async ({ targetFolderId }: TMoveFolderFormSchema) => { + if (!folder) return; + + try { + await moveFolder({ + id: folder.id, + parentId: targetFolderId ?? '', + }); + + onOpenChange(false); + + toast({ + title: 'Folder moved successfully', + }); + } catch (err) { + const error = AppError.parseError(err); + + if (error.code === AppErrorCode.NOT_FOUND) { + toast({ + title: 'Folder not found', + description: _(msg`The folder you are trying to move does not exist.`), + variant: 'destructive', + }); + + return; + } + + toast({ + title: 'Failed to move folder', + description: _(msg`An unknown error occurred while moving the folder.`), + variant: 'destructive', + }); + } + }; + + useEffect(() => { + if (!isOpen) { + form.reset(); + } + }, [isOpen, form]); + + // Filter out the current folder and only show folders of the same type + const filteredFolders = foldersData?.filter( + (f) => f.id !== folder?.id && f.type === folder?.type, + ); + + return ( + + + + Move Folder + Select a destination for this folder. + +
+ + ( + + +
+ + + {filteredFolders && + filteredFolders.map((f) => ( + + ))} +
+
+ +
+ )} + /> + + + + + + +
+
+ ); +}; diff --git a/apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx b/apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx new file mode 100644 index 000000000..886b30d41 --- /dev/null +++ b/apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx @@ -0,0 +1,176 @@ +import { useEffect } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import type * as DialogPrimitive from '@radix-ui/react-dialog'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { DocumentVisibility } from '@documenso/lib/types/document-visibility'; +import { FolderType } from '@documenso/lib/types/folder-type'; +import { trpc } from '@documenso/trpc/react'; +import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@documenso/ui/primitives/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { Input } from '@documenso/ui/primitives/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@documenso/ui/primitives/select'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +import { useOptionalCurrentTeam } from '~/providers/team'; + +export type TemplateFolderSettingsDialogProps = { + folder: TFolderWithSubfolders | null; + isOpen: boolean; + onOpenChange: (open: boolean) => void; +} & Omit; + +export const ZUpdateFolderFormSchema = z.object({ + name: z.string().min(1), + visibility: z.nativeEnum(DocumentVisibility).optional(), +}); + +export type TUpdateFolderFormSchema = z.infer; + +export const TemplateFolderSettingsDialog = ({ + folder, + isOpen, + onOpenChange, +}: TemplateFolderSettingsDialogProps) => { + const { _ } = useLingui(); + const team = useOptionalCurrentTeam(); + + const { toast } = useToast(); + const { mutateAsync: updateFolder } = trpc.folder.updateFolder.useMutation(); + + const isTeamContext = !!team; + const isTemplateFolder = folder?.type === FolderType.TEMPLATE; + + const form = useForm>({ + resolver: zodResolver(ZUpdateFolderFormSchema), + defaultValues: { + name: folder?.name ?? '', + visibility: folder?.visibility ?? DocumentVisibility.EVERYONE, + }, + }); + + useEffect(() => { + if (folder) { + form.reset({ + name: folder.name, + visibility: folder.visibility ?? DocumentVisibility.EVERYONE, + }); + } + }, [folder, form]); + + const onFormSubmit = async (data: TUpdateFolderFormSchema) => { + if (!folder) return; + + try { + await updateFolder({ + id: folder.id, + name: data.name, + visibility: + isTeamContext && !isTemplateFolder + ? (data.visibility ?? DocumentVisibility.EVERYONE) + : DocumentVisibility.EVERYONE, + }); + + toast({ + title: _(msg`Folder updated successfully`), + }); + + onOpenChange(false); + } catch (err) { + const error = AppError.parseError(err); + + if (error.code === AppErrorCode.NOT_FOUND) { + toast({ + title: _(msg`Folder not found`), + }); + } + } + }; + + return ( + + + + Folder Settings + Manage the settings for this folder. + + +
+ + ( + + Name + + + + + + )} + /> + + {isTeamContext && !isTemplateFolder && ( + ( + + Visibility + + + + )} + /> + )} + + + + + + +
+
+ ); +}; diff --git a/apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx b/apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx new file mode 100644 index 000000000..c0d068ac4 --- /dev/null +++ b/apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx @@ -0,0 +1,213 @@ +import { useEffect } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import type * as DialogPrimitive from '@radix-ui/react-dialog'; +import { FolderIcon, HomeIcon, Loader2 } from 'lucide-react'; +import { useForm } from 'react-hook-form'; +import { useNavigate } from 'react-router'; +import { z } from 'zod'; + +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { FolderType } from '@documenso/lib/types/folder-type'; +import { formatTemplatesPath } from '@documenso/lib/utils/teams'; +import { trpc } from '@documenso/trpc/react'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@documenso/ui/primitives/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +import { useOptionalCurrentTeam } from '~/providers/team'; + +export type TemplateMoveToFolderDialogProps = { + templateId: number; + templateTitle: string; + isOpen: boolean; + onOpenChange: (open: boolean) => void; + currentFolderId?: string | null; +} & Omit; + +const ZMoveTemplateFormSchema = z.object({ + folderId: z.string().nullable().optional(), +}); + +type TMoveTemplateFormSchema = z.infer; + +export function TemplateMoveToFolderDialog({ + templateId, + templateTitle, + isOpen, + onOpenChange, + currentFolderId, + ...props +}: TemplateMoveToFolderDialogProps) { + const { _ } = useLingui(); + const { toast } = useToast(); + const navigate = useNavigate(); + const team = useOptionalCurrentTeam(); + + const form = useForm({ + resolver: zodResolver(ZMoveTemplateFormSchema), + defaultValues: { + folderId: currentFolderId ?? null, + }, + }); + + const { data: folders, isLoading: isFoldersLoading } = trpc.folder.findFolders.useQuery( + { + parentId: currentFolderId ?? null, + type: FolderType.TEMPLATE, + }, + { + enabled: isOpen, + }, + ); + + const { mutateAsync: moveTemplateToFolder } = trpc.folder.moveTemplateToFolder.useMutation(); + + useEffect(() => { + if (!isOpen) { + form.reset(); + } else { + form.reset({ folderId: currentFolderId ?? null }); + } + }, [isOpen, currentFolderId, form]); + + const onSubmit = async (data: TMoveTemplateFormSchema) => { + try { + await moveTemplateToFolder({ + templateId, + folderId: data.folderId ?? null, + }); + + toast({ + title: _(msg`Template moved`), + description: _(msg`The template has been moved successfully.`), + variant: 'default', + }); + + onOpenChange(false); + + const templatesPath = formatTemplatesPath(team?.url); + + if (data.folderId) { + void navigate(`${templatesPath}/f/${data.folderId}`); + } else { + void navigate(templatesPath); + } + } catch (err) { + const error = AppError.parseError(err); + + if (error.code === AppErrorCode.NOT_FOUND) { + toast({ + title: _(msg`Error`), + description: _(msg`The folder you are trying to move the template to does not exist.`), + variant: 'destructive', + }); + + return; + } + + toast({ + title: _(msg`Error`), + description: _(msg`An error occurred while moving the template.`), + variant: 'destructive', + }); + } + }; + + return ( + + + + + Move Template to Folder + + + + Move "{templateTitle}" to a folder + + + +
+ + ( + + + Folder + + +
+ {isFoldersLoading ? ( +
+ +
+ ) : ( + <> + + + {folders?.data?.map((folder) => ( + + ))} + + )} +
+
+ +
+ )} + /> + + + + + + + + +
+
+ ); +} diff --git a/apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx b/apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx new file mode 100644 index 000000000..aadcca37e --- /dev/null +++ b/apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx @@ -0,0 +1,192 @@ +import { type ReactNode, useState } from 'react'; + +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { Loader } from 'lucide-react'; +import { useDropzone } from 'react-dropzone'; +import { Link, useNavigate, useParams } from 'react-router'; +import { match } from 'ts-pattern'; + +import { useLimits } from '@documenso/ee/server-only/limits/provider/client'; +import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; +import { useSession } from '@documenso/lib/client-only/providers/session'; +import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; +import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones'; +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions'; +import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; +import { formatDocumentsPath } from '@documenso/lib/utils/teams'; +import { trpc } from '@documenso/trpc/react'; +import { cn } from '@documenso/ui/lib/utils'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +import { useOptionalCurrentTeam } from '~/providers/team'; + +export interface DocumentDropZoneWrapperProps { + children: ReactNode; + className?: string; +} + +export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZoneWrapperProps) => { + const { _ } = useLingui(); + const { toast } = useToast(); + const { user } = useSession(); + const { folderId } = useParams(); + + const team = useOptionalCurrentTeam(); + + const navigate = useNavigate(); + const analytics = useAnalytics(); + + const [isLoading, setIsLoading] = useState(false); + + const userTimezone = + TIME_ZONES.find((timezone) => timezone === Intl.DateTimeFormat().resolvedOptions().timeZone) ?? + DEFAULT_DOCUMENT_TIME_ZONE; + + const { quota, remaining, refreshLimits } = useLimits(); + + const { mutateAsync: createDocument } = trpc.document.createDocument.useMutation(); + + const isUploadDisabled = remaining.documents === 0 || !user.emailVerified; + + const onFileDrop = async (file: File) => { + if (isUploadDisabled && IS_BILLING_ENABLED()) { + await navigate('/settings/billing'); + return; + } + + try { + setIsLoading(true); + + const response = await putPdfFile(file); + + const { id } = await createDocument({ + title: file.name, + documentDataId: response.id, + timezone: userTimezone, + folderId: folderId ?? undefined, + }); + + void refreshLimits(); + + toast({ + title: _(msg`Document uploaded`), + description: _(msg`Your document has been uploaded successfully.`), + duration: 5000, + }); + + analytics.capture('App: Document Uploaded', { + userId: user.id, + documentId: id, + timestamp: new Date().toISOString(), + }); + + await navigate( + folderId + ? `${formatDocumentsPath(team?.url)}/f/${folderId}/${id}/edit` + : `${formatDocumentsPath(team?.url)}/${id}/edit`, + ); + } catch (err) { + const error = AppError.parseError(err); + + const errorMessage = match(error.code) + .with('INVALID_DOCUMENT_FILE', () => msg`You cannot upload encrypted PDFs`) + .with( + AppErrorCode.LIMIT_EXCEEDED, + () => msg`You have reached your document limit for this month. Please upgrade your plan.`, + ) + .otherwise(() => msg`An error occurred while uploading your document.`); + + toast({ + title: _(msg`Error`), + description: _(errorMessage), + variant: 'destructive', + duration: 7500, + }); + } finally { + setIsLoading(false); + } + }; + + const onFileDropRejected = () => { + toast({ + title: _(msg`Your document failed to upload.`), + description: _(msg`File cannot be larger than ${APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB`), + duration: 5000, + variant: 'destructive', + }); + }; + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + accept: { + 'application/pdf': ['.pdf'], + }, + //disabled: isUploadDisabled, + multiple: false, + maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT), + onDrop: ([acceptedFile]) => { + if (acceptedFile) { + void onFileDrop(acceptedFile); + } + }, + onDropRejected: () => { + void onFileDropRejected(); + }, + noClick: true, + noDragEventsBubbling: true, + }); + + return ( +
+ + {children} + + {isDragActive && ( +
+
+

+ Upload Document +

+ +

+ Drag and drop your PDF file here +

+ + {isUploadDisabled && IS_BILLING_ENABLED() && ( + + Upgrade your plan to upload more documents + + )} + + {!isUploadDisabled && + team?.id === undefined && + remaining.documents > 0 && + Number.isFinite(remaining.documents) && ( +

+ + {remaining.documents} of {quota.documents} documents remaining this month. + +

+ )} +
+
+ )} + + {isLoading && ( +
+
+ +

+ Uploading document... +

+
+
+ )} +
+ ); +}; diff --git a/apps/remix/app/components/general/document/document-page-view-button.tsx b/apps/remix/app/components/general/document/document-page-view-button.tsx index 97e797976..64b12bb67 100644 --- a/apps/remix/app/components/general/document/document-page-view-button.tsx +++ b/apps/remix/app/components/general/document/document-page-view-button.tsx @@ -38,6 +38,9 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps const role = recipient?.role; const documentsPath = formatDocumentsPath(document.team?.url); + const formatPath = document.folderId + ? `${documentsPath}/f/${document.folderId}/${document.id}/edit` + : `${documentsPath}/${document.id}/edit`; const onDownloadClick = async () => { try { @@ -101,7 +104,7 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps )) .with({ isComplete: false }, () => ( diff --git a/apps/remix/app/components/general/document/document-upload.tsx b/apps/remix/app/components/general/document/document-upload.tsx index 741b6da02..66a04b014 100644 --- a/apps/remix/app/components/general/document/document-upload.tsx +++ b/apps/remix/app/components/general/document/document-upload.tsx @@ -4,7 +4,7 @@ import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import { Loader } from 'lucide-react'; -import { useNavigate } from 'react-router'; +import { useNavigate, useParams } from 'react-router'; import { match } from 'ts-pattern'; import { useLimits } from '@documenso/ee/server-only/limits/provider/client'; @@ -17,7 +17,13 @@ import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; -import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone'; +import { DocumentDropzone } from '@documenso/ui/primitives/document-upload'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@documenso/ui/primitives/tooltip'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { useOptionalCurrentTeam } from '~/providers/team'; @@ -30,6 +36,7 @@ export const DocumentUploadDropzone = ({ className }: DocumentUploadDropzoneProp const { _ } = useLingui(); const { toast } = useToast(); const { user } = useSession(); + const { folderId } = useParams(); const team = useOptionalCurrentTeam(); @@ -69,6 +76,7 @@ export const DocumentUploadDropzone = ({ className }: DocumentUploadDropzoneProp title: file.name, documentDataId: response.id, timezone: userTimezone, + folderId: folderId ?? undefined, }); void refreshLimits(); @@ -85,7 +93,11 @@ export const DocumentUploadDropzone = ({ className }: DocumentUploadDropzoneProp timestamp: new Date().toISOString(), }); - await navigate(`${formatDocumentsPath(team?.url)}/${id}/edit`); + await navigate( + folderId + ? `${formatDocumentsPath(team?.url)}/f/${folderId}/${id}/edit` + : `${formatDocumentsPath(team?.url)}/${id}/edit`, + ); } catch (err) { const error = AppError.parseError(err); @@ -121,25 +133,31 @@ export const DocumentUploadDropzone = ({ className }: DocumentUploadDropzoneProp return (
- - -
- {team?.id === undefined && - remaining.documents > 0 && - Number.isFinite(remaining.documents) && ( -

- - {remaining.documents} of {quota.documents} documents remaining this month. - -

- )} -
+ + + +
+ +
+
+ {team?.id === undefined && + remaining.documents > 0 && + Number.isFinite(remaining.documents) && ( + +

+ + {remaining.documents} of {quota.documents} documents remaining this month. + +

+
+ )} +
+
{isLoading && (
diff --git a/apps/remix/app/components/general/folder/folder-card.tsx b/apps/remix/app/components/general/folder/folder-card.tsx new file mode 100644 index 000000000..2f85174df --- /dev/null +++ b/apps/remix/app/components/general/folder/folder-card.tsx @@ -0,0 +1,88 @@ +import { FolderIcon, PinIcon } from 'lucide-react'; + +import { FolderType } from '@documenso/lib/types/folder-type'; +import { formatFolderCount } from '@documenso/lib/utils/format-folder-count'; +import { type TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema'; +import { Button } from '@documenso/ui/primitives/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@documenso/ui/primitives/dropdown-menu'; + +export type FolderCardProps = { + folder: TFolderWithSubfolders; + onNavigate: (folderId: string) => void; + onMove: (folder: TFolderWithSubfolders) => void; + onPin: (folderId: string) => void; + onUnpin: (folderId: string) => void; + onSettings: (folder: TFolderWithSubfolders) => void; + onDelete: (folder: TFolderWithSubfolders) => void; +}; + +export const FolderCard = ({ + folder, + onNavigate, + onMove, + onPin, + onUnpin, + onSettings, + onDelete, +}: FolderCardProps) => { + return ( +
+
+ + + + + + + + + onMove(folder)}>Move + {folder.pinned ? ( + onUnpin(folder.id)}>Unpin + ) : ( + onPin(folder.id)}>Pin + )} + onSettings(folder)}>Settings + onDelete(folder)}> + Delete + + + +
+
+ ); +}; diff --git a/apps/remix/app/components/general/template/template-edit-form.tsx b/apps/remix/app/components/general/template/template-edit-form.tsx index e7dcc86d8..98449958f 100644 --- a/apps/remix/app/components/general/template/template-edit-form.tsx +++ b/apps/remix/app/components/general/template/template-edit-form.tsx @@ -208,7 +208,11 @@ export const TemplateEditForm = ({ duration: 5000, }); - await navigate(templateRootPath); + const templatePath = template.folderId + ? `${templateRootPath}/f/${template.folderId}` + : templateRootPath; + + await navigate(templatePath); } catch (err) { console.error(err); diff --git a/apps/remix/app/components/tables/documents-table-action-button.tsx b/apps/remix/app/components/tables/documents-table-action-button.tsx index 20331de97..f3a605907 100644 --- a/apps/remix/app/components/tables/documents-table-action-button.tsx +++ b/apps/remix/app/components/tables/documents-table-action-button.tsx @@ -40,6 +40,9 @@ export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonPr const isCurrentTeamDocument = team && row.team?.url === team.url; const documentsPath = formatDocumentsPath(team?.url); + const formatPath = row.folderId + ? `${documentsPath}/f/${row.folderId}/${row.id}/edit` + : `${documentsPath}/${row.id}/edit`; const onDownloadClick = async () => { try { @@ -92,7 +95,7 @@ export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonPr isOwner ? { isDraft: true, isOwner: true } : { isDraft: true, isCurrentTeamDocument: true }, () => ( -
-
- {team && ( - - {team.avatarImageId && } - - {team.name.slice(0, 1)} - - - )} - -

- Documents -

-
- -
- - - {[ - ExtendedDocumentStatus.INBOX, - ExtendedDocumentStatus.PENDING, - ExtendedDocumentStatus.COMPLETED, - ExtendedDocumentStatus.DRAFT, - ExtendedDocumentStatus.ALL, - ].map((value) => ( - ( +
+ / + +
+ ))}
-
- + +
+ +
-
-
-
- {data && data.count === 0 ? ( - - ) : ( - - )} + {isFoldersLoading ? ( +
+ +
+ ) : ( + <> + {foldersData?.folders?.some((folder) => folder.pinned) && ( +
+
+ {foldersData.folders + .filter((folder) => folder.pinned) + .map((folder) => ( + { + setFolderToMove(folder); + setIsMovingFolder(true); + }} + onPin={(folderId) => void pinFolder({ folderId })} + onUnpin={(folderId) => void unpinFolder({ folderId })} + onSettings={(folder) => { + setFolderToSettings(folder); + setIsSettingsFolderOpen(true); + }} + onDelete={(folder) => { + setFolderToDelete(folder); + setIsDeletingFolder(true); + }} + /> + ))} +
+
+ )} + +
+
+ {foldersData?.folders + ?.filter((folder) => !folder.pinned) + .slice(0, 12) + .map((folder) => ( + { + setFolderToMove(folder); + setIsMovingFolder(true); + }} + onPin={(folderId) => void pinFolder({ folderId })} + onUnpin={(folderId) => void unpinFolder({ folderId })} + onSettings={(folder) => { + setFolderToSettings(folder); + setIsSettingsFolderOpen(true); + }} + onDelete={(folder) => { + setFolderToDelete(folder); + setIsDeletingFolder(true); + }} + /> + ))} +
+ +
+ {foldersData && foldersData.folders?.length > 12 && ( + + )} +
+
+ + )} + +
+
+ {team && ( + + {team.avatarImageId && } + + {team.name.slice(0, 1)} + + + )} + +

+ Documents +

+
+ +
+ + + {[ + ExtendedDocumentStatus.INBOX, + ExtendedDocumentStatus.PENDING, + ExtendedDocumentStatus.COMPLETED, + ExtendedDocumentStatus.DRAFT, + ExtendedDocumentStatus.ALL, + ].map((value) => ( + + + + + {value !== ExtendedDocumentStatus.ALL && ( + {stats[value]} + )} + + + ))} + + + + {team && } + +
+ +
+
+ +
+
+ +
+
+ {data && + data.count === 0 && + (!foldersData?.folders.length || foldersData.folders.length === 0) ? ( + + ) : ( + { + setDocumentToMove(documentId); + setIsMovingDocument(true); + }} + /> + )} +
+
+ + {documentToMove && ( + { + setIsMovingDocument(open); + + if (!open) { + setDocumentToMove(null); + } + }} + /> + )} + + { + setIsMovingFolder(open); + + if (!open) { + setFolderToMove(null); + } + }} + /> + + { + setIsSettingsFolderOpen(open); + + if (!open) { + setFolderToSettings(null); + } + }} + /> + + { + setIsDeletingFolder(open); + + if (!open) { + setFolderToDelete(null); + } + }} + />
-
+ ); } diff --git a/apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx b/apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx new file mode 100644 index 000000000..f17826f5d --- /dev/null +++ b/apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx @@ -0,0 +1,272 @@ +import { useLingui } from '@lingui/react'; +import { Plural, Trans } from '@lingui/react/macro'; +import { DocumentStatus, TeamMemberRole } from '@prisma/client'; +import { ChevronLeft, Clock9, Users2 } from 'lucide-react'; +import { Link, redirect } from 'react-router'; +import { match } from 'ts-pattern'; + +import { getSession } from '@documenso/auth/server/lib/utils/get-session'; +import { useSession } from '@documenso/lib/client-only/providers/session'; +import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; +import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document'; +import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; +import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team'; +import { DocumentVisibility } from '@documenso/lib/types/document-visibility'; +import { formatDocumentsPath } from '@documenso/lib/utils/teams'; +import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields'; +import { Badge } from '@documenso/ui/primitives/badge'; +import { Button } from '@documenso/ui/primitives/button'; +import { Card, CardContent } from '@documenso/ui/primitives/card'; +import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer'; + +import { DocumentHistorySheet } from '~/components/general/document/document-history-sheet'; +import { DocumentPageViewButton } from '~/components/general/document/document-page-view-button'; +import { DocumentPageViewDropdown } from '~/components/general/document/document-page-view-dropdown'; +import { DocumentPageViewInformation } from '~/components/general/document/document-page-view-information'; +import { DocumentPageViewRecentActivity } from '~/components/general/document/document-page-view-recent-activity'; +import { DocumentPageViewRecipients } from '~/components/general/document/document-page-view-recipients'; +import { DocumentRecipientLinkCopyDialog } from '~/components/general/document/document-recipient-link-copy-dialog'; +import { + DocumentStatus as DocumentStatusComponent, + FRIENDLY_STATUS_MAP, +} from '~/components/general/document/document-status'; +import { StackAvatarsWithTooltip } from '~/components/general/stack-avatars-with-tooltip'; +import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader'; + +import type { Route } from './+types/documents.f.$folderId.$id._index'; + +export async function loader({ params, request }: Route.LoaderArgs) { + const { user } = await getSession(request); + + let team: TGetTeamByUrlResponse | null = null; + + if (params.teamUrl) { + team = await getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl }); + } + + const { id, folderId } = params; + + const documentId = Number(id); + + const documentRootPath = formatDocumentsPath(team?.url); + + if (!documentId || !folderId) { + throw redirect(folderId ? `${documentRootPath}/f/${folderId}` : documentRootPath); + } + + const document = await getDocumentById({ + documentId, + userId: user.id, + teamId: team?.id, + folderId, + }).catch(() => null); + + if (document?.teamId && !team?.url) { + throw redirect(folderId ? `${documentRootPath}/f/${folderId}` : documentRootPath); + } + + const documentVisibility = document?.visibility; + const currentTeamMemberRole = team?.currentTeamMember?.role; + const isRecipient = document?.recipients.find((recipient) => recipient.email === user.email); + let canAccessDocument = true; + + if (team && !isRecipient && document?.userId !== user.id) { + canAccessDocument = match([documentVisibility, currentTeamMemberRole]) + .with([DocumentVisibility.EVERYONE, TeamMemberRole.ADMIN], () => true) + .with([DocumentVisibility.EVERYONE, TeamMemberRole.MANAGER], () => true) + .with([DocumentVisibility.EVERYONE, TeamMemberRole.MEMBER], () => true) + .with([DocumentVisibility.MANAGER_AND_ABOVE, TeamMemberRole.ADMIN], () => true) + .with([DocumentVisibility.MANAGER_AND_ABOVE, TeamMemberRole.MANAGER], () => true) + .with([DocumentVisibility.ADMIN, TeamMemberRole.ADMIN], () => true) + .otherwise(() => false); + } + + if (!document || !document.documentData || (team && !canAccessDocument)) { + throw redirect(folderId ? `${documentRootPath}/f/${folderId}` : documentRootPath); + } + + if (team && !canAccessDocument) { + throw redirect(folderId ? `${documentRootPath}/f/${folderId}` : documentRootPath); + } + + if (document.folderId !== folderId) { + throw redirect(folderId ? `${documentRootPath}/f/${folderId}` : documentRootPath); + } + + // Todo: Get full document instead? + const [recipients, fields] = await Promise.all([ + getRecipientsForDocument({ + documentId, + teamId: team?.id, + userId: user.id, + }), + getFieldsForDocument({ + documentId, + userId: user.id, + teamId: team?.id, + }), + ]); + + const documentWithRecipients = { + ...document, + recipients, + }; + + return superLoaderJson({ + document: documentWithRecipients, + documentRootPath, + fields, + folderId, + }); +} + +export default function DocumentPage() { + const loaderData = useSuperLoaderData(); + + const { _ } = useLingui(); + const { user } = useSession(); + + const { document, documentRootPath, fields, folderId } = loaderData; + + const { recipients, documentData, documentMeta } = document; + + // This was a feature flag. Leave to false since it's not ready. + const isDocumentHistoryEnabled = false; + + return ( +
+ {document.status === DocumentStatus.PENDING && ( + + )} + + + + Documents + + +
+
+

+ {document.title} +

+ +
+ + + {recipients.length > 0 && ( +
+ + + + + {recipients.length} Recipient(s) + + +
+ )} + + {document.deletedAt && ( + + Document deleted + + )} +
+
+ + {isDocumentHistoryEnabled && ( +
+ + + +
+ )} +
+ +
+ + + + + + + {document.status === DocumentStatus.PENDING && ( + + )} + +
+
+
+
+

+ {_(FRIENDLY_STATUS_MAP[document.status].labelExtended)} +

+ + +
+ +

+ {match(document.status) + .with(DocumentStatus.COMPLETED, () => ( + This document has been signed by all recipients + )) + .with(DocumentStatus.REJECTED, () => ( + This document has been rejected by a recipient + )) + .with(DocumentStatus.DRAFT, () => ( + This document is currently a draft and has not been sent + )) + .with(DocumentStatus.PENDING, () => { + const pendingRecipients = recipients.filter( + (recipient) => recipient.signingStatus === 'NOT_SIGNED', + ); + + return ( + + ); + }) + .exhaustive()} +

+ +
+ +
+
+ + {/* Document information section. */} + + + {/* Recipients section. */} + + + {/* Recent activity section. */} + +
+
+
+
+ ); +} diff --git a/apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.edit.tsx b/apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.edit.tsx new file mode 100644 index 000000000..b3b5067b3 --- /dev/null +++ b/apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.edit.tsx @@ -0,0 +1,155 @@ +import { Plural, Trans } from '@lingui/react/macro'; +import { TeamMemberRole } from '@prisma/client'; +import { ChevronLeft, Users2 } from 'lucide-react'; +import { Link, redirect } from 'react-router'; +import { match } from 'ts-pattern'; + +import { getSession } from '@documenso/auth/server/lib/utils/get-session'; +import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise'; +import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id'; +import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team'; +import { DocumentVisibility } from '@documenso/lib/types/document-visibility'; +import { isDocumentCompleted } from '@documenso/lib/utils/document'; +import { formatDocumentsPath } from '@documenso/lib/utils/teams'; + +import { DocumentEditForm } from '~/components/general/document/document-edit-form'; +import { DocumentStatus } from '~/components/general/document/document-status'; +import { StackAvatarsWithTooltip } from '~/components/general/stack-avatars-with-tooltip'; +import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader'; + +import type { Route } from './+types/documents.$id.edit'; + +export async function loader({ params, request }: Route.LoaderArgs) { + const { user } = await getSession(request); + + let team: TGetTeamByUrlResponse | null = null; + + if (params.teamUrl) { + team = await getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl }); + } + + const { id, folderId } = params; + + const documentId = Number(id); + + const documentRootPath = formatDocumentsPath(team?.url); + + if (!documentId || Number.isNaN(documentId)) { + throw redirect(documentRootPath); + } + + if (!folderId) { + throw redirect(documentRootPath); + } + + const document = await getDocumentWithDetailsById({ + documentId, + userId: user.id, + teamId: team?.id, + folderId, + }).catch(() => null); + + if (document?.teamId && !team?.url) { + throw redirect(documentRootPath); + } + + const documentVisibility = document?.visibility; + const currentTeamMemberRole = team?.currentTeamMember?.role; + const isRecipient = document?.recipients.find((recipient) => recipient.email === user.email); + let canAccessDocument = true; + + if (!isRecipient && document?.userId !== user.id) { + canAccessDocument = match([documentVisibility, currentTeamMemberRole]) + .with([DocumentVisibility.EVERYONE, TeamMemberRole.ADMIN], () => true) + .with([DocumentVisibility.EVERYONE, TeamMemberRole.MANAGER], () => true) + .with([DocumentVisibility.EVERYONE, TeamMemberRole.MEMBER], () => true) + .with([DocumentVisibility.MANAGER_AND_ABOVE, TeamMemberRole.ADMIN], () => true) + .with([DocumentVisibility.MANAGER_AND_ABOVE, TeamMemberRole.MANAGER], () => true) + .with([DocumentVisibility.ADMIN, TeamMemberRole.ADMIN], () => true) + .otherwise(() => false); + } + + if (!document) { + throw redirect(documentRootPath); + } + + if (team && !canAccessDocument) { + throw redirect(documentRootPath); + } + + if (document.folderId !== folderId) { + throw redirect(folderId ? `${documentRootPath}/f/${folderId}` : documentRootPath); + } + + if (isDocumentCompleted(document.status)) { + throw redirect(`${documentRootPath}/${documentId}`); + } + + const isDocumentEnterprise = await isUserEnterprise({ + userId: user.id, + teamId: team?.id, + }); + + return superLoaderJson({ + document: { + ...document, + folder: null, + }, + documentRootPath, + isDocumentEnterprise, + folderId, + }); +} + +export default function DocumentEditPage() { + const { document, documentRootPath, isDocumentEnterprise, folderId } = + useSuperLoaderData(); + + const { recipients } = document; + + return ( +
+ + + Documents + + +

+ {document.title} +

+ +
+ + + {recipients.length > 0 && ( +
+ + + + + + + +
+ )} +
+ + +
+ ); +} diff --git a/apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx b/apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx new file mode 100644 index 000000000..cee9a0c45 --- /dev/null +++ b/apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx @@ -0,0 +1,199 @@ +import type { MessageDescriptor } from '@lingui/core'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import type { Recipient } from '@prisma/client'; +import { ChevronLeft } from 'lucide-react'; +import { DateTime } from 'luxon'; +import { Link, redirect } from 'react-router'; + +import { getSession } from '@documenso/auth/server/lib/utils/get-session'; +import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; +import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; +import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team'; +import { formatDocumentsPath } from '@documenso/lib/utils/teams'; +import { Card } from '@documenso/ui/primitives/card'; + +import { DocumentAuditLogDownloadButton } from '~/components/general/document/document-audit-log-download-button'; +import { DocumentCertificateDownloadButton } from '~/components/general/document/document-certificate-download-button'; +import { + DocumentStatus as DocumentStatusComponent, + FRIENDLY_STATUS_MAP, +} from '~/components/general/document/document-status'; +import { DocumentLogsTable } from '~/components/tables/document-logs-table'; + +import type { Route } from './+types/documents.f.$folderId.$id.logs'; + +export async function loader({ params, request }: Route.LoaderArgs) { + const { user } = await getSession(request); + + let team: TGetTeamByUrlResponse | null = null; + + if (params.teamUrl) { + team = await getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl }); + } + + const { id, folderId } = params; + + const documentId = Number(id); + + const documentRootPath = formatDocumentsPath(team?.url); + + if (!documentId || Number.isNaN(documentId)) { + throw redirect(folderId ? `${documentRootPath}/f/${folderId}` : documentRootPath); + } + + if (!folderId) { + throw redirect(documentRootPath); + } + + // Todo: Get full document instead? + const [document, recipients] = await Promise.all([ + getDocumentById({ + documentId, + userId: user.id, + teamId: team?.id, + folderId, + }).catch(() => null), + getRecipientsForDocument({ + documentId, + userId: user.id, + teamId: team?.id, + }), + ]); + + if (!document || !document.documentData) { + throw redirect(folderId ? `${documentRootPath}/f/${folderId}` : documentRootPath); + } + + if (document.folderId !== folderId) { + throw redirect(documentRootPath); + } + + return { + document, + documentRootPath, + recipients, + folderId, + }; +} + +export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps) { + const { document, documentRootPath, recipients, folderId } = loaderData; + + const { _, i18n } = useLingui(); + + const documentInformation: { description: MessageDescriptor; value: string }[] = [ + { + description: msg`Document title`, + value: document.title, + }, + { + description: msg`Document ID`, + value: document.id.toString(), + }, + { + description: msg`Document status`, + value: _(FRIENDLY_STATUS_MAP[document.status].label), + }, + { + description: msg`Created by`, + value: document.user.name + ? `${document.user.name} (${document.user.email})` + : document.user.email, + }, + { + description: msg`Date created`, + value: DateTime.fromJSDate(document.createdAt) + .setLocale(i18n.locales?.[0] || i18n.locale) + .toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS), + }, + { + description: msg`Last updated`, + value: DateTime.fromJSDate(document.updatedAt) + .setLocale(i18n.locales?.[0] || i18n.locale) + .toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS), + }, + { + description: msg`Time zone`, + value: document.documentMeta?.timezone ?? 'N/A', + }, + ]; + + const formatRecipientText = (recipient: Recipient) => { + let text = recipient.email; + + if (recipient.name) { + text = `${recipient.name} (${recipient.email})`; + } + + return `[${recipient.role}] ${text}`; + }; + + return ( +
+ + + Document + + +
+
+

+ {document.title} +

+
+
+
+ +
+
+ + + +
+
+
+ +
+ + {documentInformation.map((info, i) => ( +
+

{_(info.description)}

+

{info.value}

+
+ ))} + +
+

Recipients

+
    + {recipients.map((recipient) => ( +
  • + {formatRecipientText(recipient)} +
  • + ))} +
+
+
+
+ +
+ +
+
+ ); +} diff --git a/apps/remix/app/routes/_authenticated+/documents.f.$folderId._index.tsx b/apps/remix/app/routes/_authenticated+/documents.f.$folderId._index.tsx new file mode 100644 index 000000000..ad5f40700 --- /dev/null +++ b/apps/remix/app/routes/_authenticated+/documents.f.$folderId._index.tsx @@ -0,0 +1,374 @@ +import { useEffect, useMemo, useState } from 'react'; + +import { Trans } from '@lingui/react/macro'; +import { FolderIcon, HomeIcon, Loader2 } from 'lucide-react'; +import { useNavigate, useParams, useSearchParams } from 'react-router'; +import { Link } from 'react-router'; +import { z } from 'zod'; + +import { formatAvatarUrl } from '@documenso/lib/utils/avatars'; +import { parseToIntegerArray } from '@documenso/lib/utils/params'; +import { formatDocumentsPath } from '@documenso/lib/utils/teams'; +import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; +import { trpc } from '@documenso/trpc/react'; +import { + type TFindDocumentsInternalResponse, + ZFindDocumentsInternalRequestSchema, +} from '@documenso/trpc/server/document-router/schema'; +import { type TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema'; +import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar'; +import { Button } from '@documenso/ui/primitives/button'; +import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs'; + +import { DocumentMoveToFolderDialog } from '~/components/dialogs/document-move-to-folder-dialog'; +import { CreateFolderDialog } from '~/components/dialogs/folder-create-dialog'; +import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog'; +import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog'; +import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialog'; +import { DocumentDropZoneWrapper } from '~/components/general/document/document-drop-zone-wrapper'; +import { DocumentSearch } from '~/components/general/document/document-search'; +import { DocumentStatus } from '~/components/general/document/document-status'; +import { DocumentUploadDropzone } from '~/components/general/document/document-upload'; +import { FolderCard } from '~/components/general/folder/folder-card'; +import { PeriodSelector } from '~/components/general/period-selector'; +import { DocumentsTable } from '~/components/tables/documents-table'; +import { DocumentsTableEmptyState } from '~/components/tables/documents-table-empty-state'; +import { DocumentsTableSenderFilter } from '~/components/tables/documents-table-sender-filter'; +import { useOptionalCurrentTeam } from '~/providers/team'; +import { appMetaTags } from '~/utils/meta'; + +export function meta() { + return appMetaTags('Documents'); +} + +const ZSearchParamsSchema = ZFindDocumentsInternalRequestSchema.pick({ + status: true, + period: true, + page: true, + perPage: true, + query: true, +}).extend({ + senderIds: z.string().transform(parseToIntegerArray).optional().catch([]), +}); + +export default function DocumentsPage() { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + const [isMovingDocument, setIsMovingDocument] = useState(false); + const [documentToMove, setDocumentToMove] = useState(null); + const [isMovingFolder, setIsMovingFolder] = useState(false); + const [folderToMove, setFolderToMove] = useState(null); + const [isDeletingFolder, setIsDeletingFolder] = useState(false); + const [folderToDelete, setFolderToDelete] = useState(null); + const [isSettingsFolderOpen, setIsSettingsFolderOpen] = useState(false); + const [folderToSettings, setFolderToSettings] = useState(null); + + const { folderId } = useParams(); + + const team = useOptionalCurrentTeam(); + + const { mutateAsync: pinFolder } = trpc.folder.pinFolder.useMutation(); + const { mutateAsync: unpinFolder } = trpc.folder.unpinFolder.useMutation(); + + const [stats, setStats] = useState({ + [ExtendedDocumentStatus.DRAFT]: 0, + [ExtendedDocumentStatus.PENDING]: 0, + [ExtendedDocumentStatus.COMPLETED]: 0, + [ExtendedDocumentStatus.REJECTED]: 0, + [ExtendedDocumentStatus.INBOX]: 0, + [ExtendedDocumentStatus.ALL]: 0, + }); + + const findDocumentSearchParams = useMemo( + () => ZSearchParamsSchema.safeParse(Object.fromEntries(searchParams.entries())).data || {}, + [searchParams], + ); + + const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocumentsInternal.useQuery( + { + ...findDocumentSearchParams, + folderId, + }, + ); + + const { + data: foldersData, + isLoading: isFoldersLoading, + refetch: refetchFolders, + } = trpc.folder.getFolders.useQuery({ + parentId: folderId, + }); + + useEffect(() => { + void refetch(); + void refetchFolders(); + }, [team?.url]); + + const getTabHref = (value: keyof typeof ExtendedDocumentStatus) => { + const params = new URLSearchParams(searchParams); + + params.set('status', value); + + if (value === ExtendedDocumentStatus.ALL) { + params.delete('status'); + } + + if (params.has('page')) { + params.delete('page'); + } + + return `${formatDocumentsPath(team?.url)}/f/${folderId}?${params.toString()}`; + }; + + useEffect(() => { + if (data?.stats) { + setStats(data.stats); + } + }, [data?.stats]); + + const navigateToFolder = (folderId?: string | null) => { + const documentsPath = formatDocumentsPath(team?.url); + + if (folderId) { + void navigate(`${documentsPath}/f/${folderId}`); + } else { + void navigate(documentsPath); + } + }; + + return ( + +
+
+
+ + + {foldersData?.breadcrumbs.map((folder) => ( +
+ / + +
+ ))} +
+ +
+ + +
+
+ + {isFoldersLoading ? ( +
+ +
+ ) : ( + <> + {foldersData?.folders && foldersData.folders.some((folder) => folder.pinned) && ( +
+
+ {foldersData.folders + .filter((folder) => folder.pinned) + .map((folder) => ( + { + setFolderToMove(folder); + setIsMovingFolder(true); + }} + onPin={(folderId) => void pinFolder({ folderId })} + onUnpin={(folderId) => void unpinFolder({ folderId })} + onSettings={(folder) => { + setFolderToSettings(folder); + setIsSettingsFolderOpen(true); + }} + onDelete={(folder) => { + setFolderToDelete(folder); + setIsDeletingFolder(true); + }} + /> + ))} +
+
+ )} + +
+
+ {foldersData?.folders + .filter((folder) => !folder.pinned) + .map((folder) => ( + { + setFolderToMove(folder); + setIsMovingFolder(true); + }} + onPin={(folderId) => void pinFolder({ folderId })} + onUnpin={(folderId) => void unpinFolder({ folderId })} + onSettings={(folder) => { + setFolderToSettings(folder); + setIsSettingsFolderOpen(true); + }} + onDelete={(folder) => { + setFolderToDelete(folder); + setIsDeletingFolder(true); + }} + /> + ))} +
+
+ + )} + +
+
+ {team && ( + + {team.avatarImageId && } + + {team.name.slice(0, 1)} + + + )} + +

+ Documents +

+
+ +
+ + + {[ + ExtendedDocumentStatus.INBOX, + ExtendedDocumentStatus.PENDING, + ExtendedDocumentStatus.COMPLETED, + ExtendedDocumentStatus.DRAFT, + ExtendedDocumentStatus.ALL, + ].map((value) => ( + + + + + {value !== ExtendedDocumentStatus.ALL && ( + {stats[value]} + )} + + + ))} + + + + {team && } + +
+ +
+
+ +
+
+
+ +
+
+ {data && + data.count === 0 && + (!foldersData?.folders.length || foldersData.folders.length === 0) ? ( + + ) : ( + { + setDocumentToMove(documentId); + setIsMovingDocument(true); + }} + /> + )} +
+
+ + {documentToMove && ( + { + setIsMovingDocument(open); + + if (!open) { + setDocumentToMove(null); + } + }} + currentFolderId={folderId} + /> + )} + + { + setIsMovingFolder(open); + + if (!open) { + setFolderToMove(null); + } + }} + /> + + { + setIsSettingsFolderOpen(open); + + if (!open) { + setFolderToSettings(null); + } + }} + /> + + { + setIsDeletingFolder(open); + + if (!open) { + setFolderToDelete(null); + } + }} + /> +
+
+ ); +} diff --git a/apps/remix/app/routes/_authenticated+/documents.folders._index.tsx b/apps/remix/app/routes/_authenticated+/documents.folders._index.tsx new file mode 100644 index 000000000..335502781 --- /dev/null +++ b/apps/remix/app/routes/_authenticated+/documents.folders._index.tsx @@ -0,0 +1,181 @@ +import { useState } from 'react'; + +import { Trans } from '@lingui/react/macro'; +import { HomeIcon, Loader2 } from 'lucide-react'; +import { useNavigate } from 'react-router'; + +import { FolderType } from '@documenso/lib/types/folder-type'; +import { formatDocumentsPath } from '@documenso/lib/utils/teams'; +import { trpc } from '@documenso/trpc/react'; +import { type TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema'; +import { Button } from '@documenso/ui/primitives/button'; + +import { CreateFolderDialog } from '~/components/dialogs/folder-create-dialog'; +import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog'; +import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog'; +import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialog'; +import { FolderCard } from '~/components/general/folder/folder-card'; +import { appMetaTags } from '~/utils/meta'; + +export function meta() { + return appMetaTags('Documents'); +} + +export default function DocumentsFoldersPage() { + const navigate = useNavigate(); + const [isMovingFolder, setIsMovingFolder] = useState(false); + const [folderToMove, setFolderToMove] = useState(null); + const [isDeletingFolder, setIsDeletingFolder] = useState(false); + const [folderToDelete, setFolderToDelete] = useState(null); + const [isSettingsFolderOpen, setIsSettingsFolderOpen] = useState(false); + const [folderToSettings, setFolderToSettings] = useState(null); + + const { data: foldersData, isLoading: isFoldersLoading } = trpc.folder.getFolders.useQuery({ + type: FolderType.DOCUMENT, + parentId: null, + }); + + const { mutateAsync: pinFolder } = trpc.folder.pinFolder.useMutation(); + const { mutateAsync: unpinFolder } = trpc.folder.unpinFolder.useMutation(); + + const navigateToFolder = (folderId?: string | null) => { + const documentsPath = formatDocumentsPath(); + + if (folderId) { + void navigate(`${documentsPath}/f/${folderId}`); + } else { + void navigate(documentsPath); + } + }; + + return ( +
+
+
+ +
+ +
+ +
+
+ +
+ {isFoldersLoading ? ( +
+ +
+ ) : ( + <> + {foldersData?.folders?.some((folder) => folder.pinned) && ( +
+
+ {foldersData.folders + .filter((folder) => folder.pinned) + .map((folder) => ( + { + setFolderToMove(folder); + setIsMovingFolder(true); + }} + onPin={(folderId) => void pinFolder({ folderId })} + onUnpin={(folderId) => void unpinFolder({ folderId })} + onSettings={(folder) => { + setFolderToSettings(folder); + setIsSettingsFolderOpen(true); + }} + onDelete={(folder) => { + setFolderToDelete(folder); + setIsDeletingFolder(true); + }} + /> + ))} +
+
+ )} + +
+

+ All Folders +

+ +
+ {foldersData?.folders + .filter((folder) => !folder.pinned) + .map((folder) => ( + { + setFolderToMove(folder); + setIsMovingFolder(true); + }} + onPin={(folderId) => void pinFolder({ folderId })} + onUnpin={(folderId) => void unpinFolder({ folderId })} + onSettings={(folder) => { + setFolderToSettings(folder); + setIsSettingsFolderOpen(true); + }} + onDelete={(folder) => { + setFolderToDelete(folder); + setIsDeletingFolder(true); + }} + /> + ))} +
+
+ + )} +
+ + { + setIsMovingFolder(open); + + if (!open) { + setFolderToMove(null); + } + }} + /> + + { + setIsSettingsFolderOpen(open); + + if (!open) { + setFolderToSettings(null); + } + }} + /> + + { + setIsDeletingFolder(open); + + if (!open) { + setFolderToDelete(null); + } + }} + /> +
+ ); +} diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId.$id._index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId.$id._index.tsx new file mode 100644 index 000000000..35c8104c4 --- /dev/null +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId.$id._index.tsx @@ -0,0 +1,5 @@ +import DocumentPage, { loader } from '~/routes/_authenticated+/documents.f.$folderId.$id._index'; + +export { loader }; + +export default DocumentPage; diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId.$id.edit.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId.$id.edit.tsx new file mode 100644 index 000000000..1227e088f --- /dev/null +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId.$id.edit.tsx @@ -0,0 +1,5 @@ +import DocumentEditPage, { loader } from '~/routes/_authenticated+/documents.f.$folderId.$id.edit'; + +export { loader }; + +export default DocumentEditPage; diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId.$id.logs.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId.$id.logs.tsx new file mode 100644 index 000000000..ed1abe1e0 --- /dev/null +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId.$id.logs.tsx @@ -0,0 +1,5 @@ +import DocumentLogsPage, { loader } from '~/routes/_authenticated+/documents.f.$folderId.$id.logs'; + +export { loader }; + +export default DocumentLogsPage; diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId._index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId._index.tsx new file mode 100644 index 000000000..05707e676 --- /dev/null +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId._index.tsx @@ -0,0 +1,5 @@ +import DocumentsPage, { meta } from '~/routes/_authenticated+/documents.f.$folderId._index'; + +export { meta }; + +export default DocumentsPage; diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx new file mode 100644 index 000000000..0dd6e5103 --- /dev/null +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx @@ -0,0 +1,5 @@ +import DocumentsFoldersPage, { meta } from '~/routes/_authenticated+/documents.folders._index'; + +export { meta }; + +export default DocumentsFoldersPage; diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId.$id._index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId.$id._index.tsx new file mode 100644 index 000000000..581561fc9 --- /dev/null +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId.$id._index.tsx @@ -0,0 +1,5 @@ +import TemplatePage, { loader } from '~/routes/_authenticated+/templates.f.$folderId.$id._index'; + +export { loader }; + +export default TemplatePage; diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId.$id.edit.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId.$id.edit.tsx new file mode 100644 index 000000000..0de6a0574 --- /dev/null +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId.$id.edit.tsx @@ -0,0 +1,5 @@ +import TemplateEditPage, { loader } from '~/routes/_authenticated+/templates.f.$folderId.$id.edit'; + +export { loader }; + +export default TemplateEditPage; diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx new file mode 100644 index 000000000..5bacbff06 --- /dev/null +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx @@ -0,0 +1,5 @@ +import TemplatesPage, { meta } from '~/routes/_authenticated+/templates.f.$folderId._index'; + +export { meta }; + +export default TemplatesPage; diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx new file mode 100644 index 000000000..55c8ef207 --- /dev/null +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx @@ -0,0 +1,5 @@ +import TemplatesFoldersPage, { meta } from '~/routes/_authenticated+/templates.folders._index'; + +export { meta }; + +export default TemplatesFoldersPage; diff --git a/apps/remix/app/routes/_authenticated+/templates.$id._index.tsx b/apps/remix/app/routes/_authenticated+/templates.$id._index.tsx index e315b87cc..9000938cf 100644 --- a/apps/remix/app/routes/_authenticated+/templates.$id._index.tsx +++ b/apps/remix/app/routes/_authenticated+/templates.$id._index.tsx @@ -55,6 +55,10 @@ export async function loader({ params, request }: Route.LoaderArgs) { throw redirect(templateRootPath); } + if (template.folderId) { + throw redirect(`${templateRootPath}/f/${template.folderId}/${templateId}`); + } + return superLoaderJson({ user, team, diff --git a/apps/remix/app/routes/_authenticated+/templates.$id.edit.tsx b/apps/remix/app/routes/_authenticated+/templates.$id.edit.tsx index 75c0c8f12..b746b6ba5 100644 --- a/apps/remix/app/routes/_authenticated+/templates.$id.edit.tsx +++ b/apps/remix/app/routes/_authenticated+/templates.$id.edit.tsx @@ -45,13 +45,20 @@ export async function loader({ params, request }: Route.LoaderArgs) { throw redirect(templateRootPath); } + if (template.folderId) { + throw redirect(`${templateRootPath}/f/${template.folderId}/${templateId}/edit`); + } + const isTemplateEnterprise = await isUserEnterprise({ userId: user.id, teamId: team?.id, }); return superLoaderJson({ - template, + template: { + ...template, + folder: null, + }, isTemplateEnterprise, templateRootPath, }); @@ -65,7 +72,11 @@ export default function TemplateEditPage() {
diff --git a/apps/remix/app/routes/_authenticated+/templates._index.tsx b/apps/remix/app/routes/_authenticated+/templates._index.tsx index edca92697..649620173 100644 --- a/apps/remix/app/routes/_authenticated+/templates._index.tsx +++ b/apps/remix/app/routes/_authenticated+/templates._index.tsx @@ -1,15 +1,23 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { Trans } from '@lingui/react/macro'; -import { Bird } from 'lucide-react'; -import { useSearchParams } from 'react-router'; +import { Bird, FolderIcon, HomeIcon, Loader2 } from 'lucide-react'; +import { useNavigate, useSearchParams } from 'react-router'; +import { FolderType } from '@documenso/lib/types/folder-type'; import { formatAvatarUrl } from '@documenso/lib/utils/avatars'; import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams'; import { trpc } from '@documenso/trpc/react'; +import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema'; import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar'; +import { Button } from '@documenso/ui/primitives/button'; import { TemplateCreateDialog } from '~/components/dialogs/template-create-dialog'; +import { TemplateFolderCreateDialog } from '~/components/dialogs/template-folder-create-dialog'; +import { TemplateFolderDeleteDialog } from '~/components/dialogs/template-folder-delete-dialog'; +import { TemplateFolderMoveDialog } from '~/components/dialogs/template-folder-move-dialog'; +import { TemplateFolderSettingsDialog } from '~/components/dialogs/template-folder-settings-dialog'; +import { FolderCard } from '~/components/general/folder/folder-card'; import { TemplatesTable } from '~/components/tables/templates-table'; import { useOptionalCurrentTeam } from '~/providers/team'; import { appMetaTags } from '~/utils/meta'; @@ -19,10 +27,21 @@ export function meta() { } export default function TemplatesPage() { + const navigate = useNavigate(); const [searchParams] = useSearchParams(); + const [isMovingFolder, setIsMovingFolder] = useState(false); + const [folderToMove, setFolderToMove] = useState(null); + const [isDeletingFolder, setIsDeletingFolder] = useState(false); + const [folderToDelete, setFolderToDelete] = useState(null); + const [isSettingsFolderOpen, setIsSettingsFolderOpen] = useState(false); + const [folderToSettings, setFolderToSettings] = useState(null); + const team = useOptionalCurrentTeam(); + const { mutateAsync: pinFolder } = trpc.folder.pinFolder.useMutation(); + const { mutateAsync: unpinFolder } = trpc.folder.unpinFolder.useMutation(); + const page = Number(searchParams.get('page')) || 1; const perPage = Number(searchParams.get('perPage')) || 10; @@ -34,19 +53,165 @@ export default function TemplatesPage() { perPage: perPage, }); - // Refetch the templates when the team URL changes. + const { + data: foldersData, + isLoading: isFoldersLoading, + refetch: refetchFolders, + } = trpc.folder.getFolders.useQuery({ + type: FolderType.TEMPLATE, + parentId: null, + }); + useEffect(() => { void refetch(); + void refetchFolders(); }, [team?.url]); + const navigateToFolder = (folderId?: string | null) => { + const templatesPath = formatTemplatesPath(team?.url); + + if (folderId) { + void navigate(`${templatesPath}/f/${folderId}`); + } else { + void navigate(templatesPath); + } + }; + + const handleNavigate = (folderId: string) => { + navigateToFolder(folderId); + }; + + const handleMove = (folder: TFolderWithSubfolders) => { + setFolderToMove(folder); + setIsMovingFolder(true); + }; + + const handlePin = (folderId: string) => { + void pinFolder({ folderId }); + }; + + const handleUnpin = (folderId: string) => { + void unpinFolder({ folderId }); + }; + + const handleSettings = (folder: TFolderWithSubfolders) => { + setFolderToSettings(folder); + setIsSettingsFolderOpen(true); + }; + + const handleDelete = (folder: TFolderWithSubfolders) => { + setFolderToDelete(folder); + setIsDeletingFolder(true); + }; + + const handleViewAllFolders = () => { + void navigate(`${formatTemplatesPath(team?.url)}/folders`); + }; + return (
-
+
+
+ + + {foldersData?.breadcrumbs.map((folder) => ( +
+ / + +
+ ))} +
+ +
+ + +
+
+ + {isFoldersLoading ? ( +
+ +
+ ) : ( + <> + {foldersData?.folders && foldersData.folders.some((folder) => folder.pinned) && ( +
+
+ {foldersData.folders + .filter((folder) => folder.pinned) + .map((folder) => ( + + ))} +
+
+ )} + +
+
+ {foldersData?.folders + ?.filter((folder) => !folder.pinned) + .slice(0, 12) + .map((folder) => ( + + ))} +
+ +
+ {foldersData && foldersData.folders?.length > 12 && ( + + )} +
+
+ + )} + +
{team && ( {team.avatarImageId && } - + {team.name.slice(0, 1)} @@ -57,38 +222,71 @@ export default function TemplatesPage() {

-
- +
+ {data && data.count === 0 ? ( +
+ + +
+

+ We're all empty +

+ +

+ + You have not yet created any templates. To create a template please upload one. + +

+
+
+ ) : ( + + )}
-
- {data && data.count === 0 ? ( -
- + { + setIsMovingFolder(open); -
-

- We're all empty -

+ if (!open) { + setFolderToMove(null); + } + }} + /> -

- - You have not yet created any templates. To create a template please upload one. - -

-
-
- ) : ( - - )} -
+ { + setIsSettingsFolderOpen(open); + + if (!open) { + setFolderToSettings(null); + } + }} + /> + + { + setIsDeletingFolder(open); + + if (!open) { + setFolderToDelete(null); + } + }} + />
); } diff --git a/apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx b/apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx new file mode 100644 index 000000000..9aa257824 --- /dev/null +++ b/apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx @@ -0,0 +1,230 @@ +import { Trans } from '@lingui/react/macro'; +import { DocumentSigningOrder, SigningStatus } from '@prisma/client'; +import { ChevronLeft, LucideEdit } from 'lucide-react'; +import { Link, redirect, useNavigate } from 'react-router'; + +import { getSession } from '@documenso/auth/server/lib/utils/get-session'; +import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team'; +import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id'; +import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams'; +import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields'; +import { Button } from '@documenso/ui/primitives/button'; +import { Card, CardContent } from '@documenso/ui/primitives/card'; +import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer'; + +import { TemplateBulkSendDialog } from '~/components/dialogs/template-bulk-send-dialog'; +import { TemplateDirectLinkDialogWrapper } from '~/components/dialogs/template-direct-link-dialog-wrapper'; +import { TemplateUseDialog } from '~/components/dialogs/template-use-dialog'; +import { TemplateDirectLinkBadge } from '~/components/general/template/template-direct-link-badge'; +import { TemplatePageViewDocumentsTable } from '~/components/general/template/template-page-view-documents-table'; +import { TemplatePageViewInformation } from '~/components/general/template/template-page-view-information'; +import { TemplatePageViewRecentActivity } from '~/components/general/template/template-page-view-recent-activity'; +import { TemplatePageViewRecipients } from '~/components/general/template/template-page-view-recipients'; +import { TemplateType } from '~/components/general/template/template-type'; +import { TemplatesTableActionDropdown } from '~/components/tables/templates-table-action-dropdown'; +import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader'; + +import type { Route } from './+types/templates.$id._index'; + +export async function loader({ params, request }: Route.LoaderArgs) { + const { user } = await getSession(request); + + let team: TGetTeamByUrlResponse | null = null; + + if (params.teamUrl) { + team = await getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl }); + } + + const { id, folderId } = params; + + const templateId = Number(id); + const templateRootPath = formatTemplatesPath(team?.url); + const documentRootPath = formatDocumentsPath(team?.url); + + if (!templateId || Number.isNaN(templateId)) { + throw redirect(folderId ? `${documentRootPath}/f/${folderId}` : documentRootPath); + } + + const template = await getTemplateById({ + id: templateId, + userId: user.id, + teamId: team?.id, + folderId, + }).catch(() => null); + + if (!template || !template.templateDocumentData || (template?.teamId && !team?.url)) { + throw redirect(folderId ? `${templateRootPath}/f/${folderId}` : templateRootPath); + } + + if (!template.folderId) { + throw redirect(`${templateRootPath}/${templateId}`); + } + + if (template.folderId !== folderId) { + throw redirect(`${templateRootPath}/f/${template.folderId}/${templateId}`); + } + + return superLoaderJson({ + user, + team, + template, + templateRootPath, + documentRootPath, + }); +} + +export default function TemplatePage() { + const { user, team, template, templateRootPath, documentRootPath } = + useSuperLoaderData(); + + const { templateDocumentData, fields, recipients, templateMeta } = template; + + const navigate = useNavigate(); + + // Remap to fit the DocumentReadOnlyFields component. + const readOnlyFields = fields.map((field) => { + const recipient = recipients.find((recipient) => recipient.id === field.recipientId) || { + name: '', + email: '', + signingStatus: SigningStatus.NOT_SIGNED, + }; + + return { + ...field, + recipient, + signature: null, + }; + }); + + const mockedDocumentMeta = templateMeta + ? { + ...templateMeta, + signingOrder: templateMeta.signingOrder || DocumentSigningOrder.SEQUENTIAL, + documentId: 0, + } + : undefined; + + return ( +
+ + + Templates + + +
+
+

+ {template.title} +

+ +
+ + + {template.directLink?.token && ( + + )} +
+
+ +
+ + + + + +
+
+ +
+ + + + + + + + +
+
+
+
+

+ Template +

+ +
+ navigate(templateRootPath)} + onMove={async ({ teamUrl, templateId }) => + navigate(`${formatTemplatesPath(teamUrl)}/${templateId}`) + } + /> +
+
+ +

+ Manage and view template +

+ +
+ + Use + + } + /> +
+
+ + {/* Template information section. */} + + + {/* Recipients section. */} + + + {/* Recent activity section. */} + +
+
+
+ +
+

+ Documents created from template +

+ + +
+
+ ); +} diff --git a/apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id.edit.tsx b/apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id.edit.tsx new file mode 100644 index 000000000..2c24f0df7 --- /dev/null +++ b/apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id.edit.tsx @@ -0,0 +1,122 @@ +import { Trans } from '@lingui/react/macro'; +import { ChevronLeft } from 'lucide-react'; +import { Link, redirect } from 'react-router'; + +import { getSession } from '@documenso/auth/server/lib/utils/get-session'; +import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise'; +import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team'; +import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id'; +import { formatTemplatesPath } from '@documenso/lib/utils/teams'; + +import { TemplateDirectLinkBadge } from '~/components/general/template/template-direct-link-badge'; +import { TemplateEditForm } from '~/components/general/template/template-edit-form'; +import { TemplateType } from '~/components/general/template/template-type'; +import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader'; + +import { TemplateDirectLinkDialogWrapper } from '../../components/dialogs/template-direct-link-dialog-wrapper'; +import type { Route } from './+types/templates.$id.edit'; + +export async function loader({ params, request }: Route.LoaderArgs) { + const { user } = await getSession(request); + + let team: TGetTeamByUrlResponse | null = null; + + if (params.teamUrl) { + team = await getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl }); + } + + const { id } = params; + + const templateId = Number(id); + const templateRootPath = formatTemplatesPath(team?.url); + + if (!templateId || Number.isNaN(templateId)) { + throw redirect(templateRootPath); + } + + const template = await getTemplateById({ + id: templateId, + userId: user.id, + teamId: team?.id, + }).catch(() => null); + + if (!template || !template.templateDocumentData) { + throw redirect(templateRootPath); + } + + if (!template.folderId) { + throw redirect(`${templateRootPath}/${templateId}/edit`); + } + + if (template.folderId !== params.folderId) { + throw redirect(`${templateRootPath}/f/${template.folderId}/${templateId}/edit`); + } + + const isTemplateEnterprise = await isUserEnterprise({ + userId: user.id, + teamId: team?.id, + }); + + return superLoaderJson({ + template: { + ...template, + folder: null, + }, + isTemplateEnterprise, + templateRootPath, + }); +} + +export default function TemplateEditPage() { + const { template, isTemplateEnterprise, templateRootPath } = useSuperLoaderData(); + + return ( +
+
+
+ + + Template + + +

+ {template.title} +

+ +
+ + + {template.directLink?.token && ( + + )} +
+
+ +
+ +
+
+ + +
+ ); +} diff --git a/apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx b/apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx new file mode 100644 index 000000000..f341e7a83 --- /dev/null +++ b/apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx @@ -0,0 +1,369 @@ +import { useEffect, useState } from 'react'; + +import { Trans } from '@lingui/react/macro'; +import { Bird, FolderIcon, HomeIcon, Loader2, PinIcon } from 'lucide-react'; +import { useNavigate, useParams, useSearchParams } from 'react-router'; + +import { FolderType } from '@documenso/lib/types/folder-type'; +import { formatAvatarUrl } from '@documenso/lib/utils/avatars'; +import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams'; +import { trpc } from '@documenso/trpc/react'; +import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema'; +import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar'; +import { Button } from '@documenso/ui/primitives/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@documenso/ui/primitives/dropdown-menu'; + +import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog'; +import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog'; +import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialog'; +import { TemplateCreateDialog } from '~/components/dialogs/template-create-dialog'; +import { TemplateFolderCreateDialog } from '~/components/dialogs/template-folder-create-dialog'; +import { TemplatesTable } from '~/components/tables/templates-table'; +import { useOptionalCurrentTeam } from '~/providers/team'; +import { appMetaTags } from '~/utils/meta'; + +export function meta() { + return appMetaTags('Templates'); +} + +export default function TemplatesPage() { + const [searchParams] = useSearchParams(); + const { folderId } = useParams(); + const navigate = useNavigate(); + + const team = useOptionalCurrentTeam(); + + const page = Number(searchParams.get('page')) || 1; + const perPage = Number(searchParams.get('perPage')) || 10; + + const documentRootPath = formatDocumentsPath(team?.url); + const templateRootPath = formatTemplatesPath(team?.url); + + const { data, isLoading, isLoadingError, refetch } = trpc.template.findTemplates.useQuery({ + page: page, + perPage: perPage, + folderId: folderId, + }); + + const { + data: foldersData, + isLoading: isFoldersLoading, + refetch: refetchFolders, + } = trpc.folder.getFolders.useQuery({ + parentId: folderId, + type: FolderType.TEMPLATE, + }); + + const { mutateAsync: pinFolder } = trpc.folder.pinFolder.useMutation(); + const { mutateAsync: unpinFolder } = trpc.folder.unpinFolder.useMutation(); + + const [folderToMove, setFolderToMove] = useState(null); + const [isMovingFolder, setIsMovingFolder] = useState(false); + const [folderToSettings, setFolderToSettings] = useState(null); + const [isSettingsFolderOpen, setIsSettingsFolderOpen] = useState(false); + const [folderToDelete, setFolderToDelete] = useState(null); + const [isDeletingFolder, setIsDeletingFolder] = useState(false); + + useEffect(() => { + void refetch(); + void refetchFolders(); + }, [team?.url]); + + const navigateToFolder = (folderId?: string) => { + const templatesPath = formatTemplatesPath(team?.url); + + if (folderId) { + void navigate(`${templatesPath}/f/${folderId}`); + } else { + void navigate(templatesPath); + } + }; + + return ( +
+
+
+ + + {foldersData?.breadcrumbs.map((folder) => ( +
+ / + +
+ ))} +
+ +
+ + +
+
+ + {isFoldersLoading ? ( +
+ +
+ ) : ( + <> + {foldersData?.folders.some((folder) => folder.pinned) && ( +
+
+ {foldersData?.folders + .filter((folder) => folder.pinned) + .map((folder) => ( +
+
+ + + + + + + + { + setFolderToMove(folder); + setIsMovingFolder(true); + }} + > + Move + + { + void unpinFolder({ folderId: folder.id }); + }} + > + Unpin + + { + setFolderToSettings(folder); + setIsSettingsFolderOpen(true); + }} + > + Settings + + { + setFolderToDelete(folder); + setIsDeletingFolder(true); + }} + > + Delete + + + +
+
+ ))} +
+
+ )} + +
+ {foldersData?.folders + .filter((folder) => !folder.pinned) + .map((folder) => ( +
+
+ + + + + + + + { + setFolderToMove(folder); + setIsMovingFolder(true); + }} + > + Move + + { + void pinFolder({ folderId: folder.id }); + }} + > + Pin + + { + setFolderToSettings(folder); + setIsSettingsFolderOpen(true); + }} + > + Settings + + { + setFolderToDelete(folder); + setIsDeletingFolder(true); + }} + > + Delete + + + +
+
+ ))} +
+ + )} + +
+
+ {team && ( + + {team.avatarImageId && } + + {team.name.slice(0, 1)} + + + )} + +

+ Templates +

+
+ +
+ {data && data.count === 0 ? ( +
+ + +
+

+ We're all empty +

+ +

+ + You have not yet created any templates. To create a template please upload one. + +

+
+
+ ) : ( + + )} +
+
+ + { + setIsMovingFolder(open); + + if (!open) { + setFolderToMove(null); + } + }} + /> + + { + setIsSettingsFolderOpen(open); + + if (!open) { + setFolderToSettings(null); + } + }} + /> + + { + setIsDeletingFolder(open); + + if (!open) { + setFolderToDelete(null); + } + }} + /> +
+ ); +} diff --git a/apps/remix/app/routes/_authenticated+/templates.folders._index.tsx b/apps/remix/app/routes/_authenticated+/templates.folders._index.tsx new file mode 100644 index 000000000..b58194098 --- /dev/null +++ b/apps/remix/app/routes/_authenticated+/templates.folders._index.tsx @@ -0,0 +1,181 @@ +import { useState } from 'react'; + +import { Trans } from '@lingui/react/macro'; +import { HomeIcon, Loader2 } from 'lucide-react'; +import { useNavigate } from 'react-router'; + +import { FolderType } from '@documenso/lib/types/folder-type'; +import { formatTemplatesPath } from '@documenso/lib/utils/teams'; +import { trpc } from '@documenso/trpc/react'; +import { type TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema'; +import { Button } from '@documenso/ui/primitives/button'; + +import { TemplateFolderCreateDialog } from '~/components/dialogs/template-folder-create-dialog'; +import { TemplateFolderDeleteDialog } from '~/components/dialogs/template-folder-delete-dialog'; +import { TemplateFolderMoveDialog } from '~/components/dialogs/template-folder-move-dialog'; +import { TemplateFolderSettingsDialog } from '~/components/dialogs/template-folder-settings-dialog'; +import { FolderCard } from '~/components/general/folder/folder-card'; +import { appMetaTags } from '~/utils/meta'; + +export function meta() { + return appMetaTags('Templates'); +} + +export default function TemplatesFoldersPage() { + const navigate = useNavigate(); + const [isMovingFolder, setIsMovingFolder] = useState(false); + const [folderToMove, setFolderToMove] = useState(null); + const [isDeletingFolder, setIsDeletingFolder] = useState(false); + const [folderToDelete, setFolderToDelete] = useState(null); + const [isSettingsFolderOpen, setIsSettingsFolderOpen] = useState(false); + const [folderToSettings, setFolderToSettings] = useState(null); + + const { data: foldersData, isLoading: isFoldersLoading } = trpc.folder.getFolders.useQuery({ + type: FolderType.TEMPLATE, + parentId: null, + }); + + const { mutateAsync: pinFolder } = trpc.folder.pinFolder.useMutation(); + const { mutateAsync: unpinFolder } = trpc.folder.unpinFolder.useMutation(); + + const navigateToFolder = (folderId?: string | null) => { + const templatesPath = formatTemplatesPath(); + + if (folderId) { + void navigate(`${templatesPath}/f/${folderId}`); + } else { + void navigate(templatesPath); + } + }; + + return ( +
+
+
+ +
+ +
+ +
+
+ +
+ {isFoldersLoading ? ( +
+ +
+ ) : ( + <> + {foldersData?.folders?.some((folder) => folder.pinned) && ( +
+
+ {foldersData.folders + .filter((folder) => folder.pinned) + .map((folder) => ( + { + setFolderToMove(folder); + setIsMovingFolder(true); + }} + onPin={(folderId) => void pinFolder({ folderId })} + onUnpin={(folderId) => void unpinFolder({ folderId })} + onSettings={(folder) => { + setFolderToSettings(folder); + setIsSettingsFolderOpen(true); + }} + onDelete={(folder) => { + setFolderToDelete(folder); + setIsDeletingFolder(true); + }} + /> + ))} +
+
+ )} + +
+

+ All Folders +

+ +
+ {foldersData?.folders + .filter((folder) => !folder.pinned) + .map((folder) => ( + { + setFolderToMove(folder); + setIsMovingFolder(true); + }} + onPin={(folderId) => void pinFolder({ folderId })} + onUnpin={(folderId) => void unpinFolder({ folderId })} + onSettings={(folder) => { + setFolderToSettings(folder); + setIsSettingsFolderOpen(true); + }} + onDelete={(folder) => { + setFolderToDelete(folder); + setIsDeletingFolder(true); + }} + /> + ))} +
+
+ + )} +
+ + { + setIsMovingFolder(open); + + if (!open) { + setFolderToMove(null); + } + }} + /> + + { + setIsSettingsFolderOpen(open); + + if (!open) { + setFolderToSettings(null); + } + }} + /> + + { + setIsDeletingFolder(open); + + if (!open) { + setFolderToDelete(null); + } + }} + /> +
+ ); +} diff --git a/apps/remix/app/routes/_recipient+/d.$token+/_index.tsx b/apps/remix/app/routes/_recipient+/d.$token+/_index.tsx index a93a01560..21bfab597 100644 --- a/apps/remix/app/routes/_recipient+/d.$token+/_index.tsx +++ b/apps/remix/app/routes/_recipient+/d.$token+/_index.tsx @@ -60,7 +60,10 @@ export async function loader({ params, request }: Route.LoaderArgs) { return superLoaderJson({ isAccessAuthValid: true, - template, + template: { + ...template, + folder: null, + }, directTemplateRecipient, } as const); } diff --git a/packages/app-tests/e2e/document-flow/stepper-component.spec.ts b/packages/app-tests/e2e/document-flow/stepper-component.spec.ts index 94bb5320f..169b5abce 100644 --- a/packages/app-tests/e2e/document-flow/stepper-component.spec.ts +++ b/packages/app-tests/e2e/document-flow/stepper-component.spec.ts @@ -45,11 +45,14 @@ test('[DOCUMENT_FLOW]: should be able to upload a PDF document', async ({ page } // Upload document. const [fileChooser] = await Promise.all([ page.waitForEvent('filechooser'), - page.locator('input[type=file]').evaluate((e) => { - if (e instanceof HTMLInputElement) { - e.click(); - } - }), + page + .locator('input[type=file]') + .nth(1) + .evaluate((e) => { + if (e instanceof HTMLInputElement) { + e.click(); + } + }), ]); await fileChooser.setFiles(path.join(__dirname, '../../../../assets/example.pdf')); @@ -641,7 +644,7 @@ test('[DOCUMENT_FLOW]: should prevent out-of-order signing in sequential mode', }) => { const user = await seedUser(); - const { document, recipients } = await seedPendingDocumentWithFullFields({ + const { recipients } = await seedPendingDocumentWithFullFields({ owner: user, recipients: ['user1@example.com', 'user2@example.com', 'user3@example.com'], fields: [FieldType.SIGNATURE], diff --git a/packages/app-tests/e2e/folders/individual-account-folders.spec.ts b/packages/app-tests/e2e/folders/individual-account-folders.spec.ts new file mode 100644 index 000000000..db65bc7b9 --- /dev/null +++ b/packages/app-tests/e2e/folders/individual-account-folders.spec.ts @@ -0,0 +1,842 @@ +import { expect, test } from '@playwright/test'; +import path from 'node:path'; + +import { FolderType } from '@documenso/prisma/client'; +import { seedBlankDocument } from '@documenso/prisma/seed/documents'; +import { seedBlankFolder } from '@documenso/prisma/seed/folders'; +import { seedBlankTemplate } from '@documenso/prisma/seed/templates'; +import { seedUser } from '@documenso/prisma/seed/users'; + +import { apiSignin } from '../fixtures/authentication'; + +test.describe.configure({ mode: 'parallel' }); + +test('create folder button is visible on documents page', async ({ page }) => { + const user = await seedUser(); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/', + }); + + await expect(page.getByRole('button', { name: 'Create Folder' })).toBeVisible(); +}); + +test('user can create a document folder', async ({ page }) => { + const user = await seedUser(); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/', + }); + + await page.getByRole('button', { name: 'Create Folder' }).click(); + await expect(page.getByRole('dialog', { name: 'Create New folder' })).toBeVisible(); + + await page.getByLabel('Folder name').fill('My folder'); + await page.getByRole('button', { name: 'Create' }).click(); + + await page.waitForTimeout(1000); + + await expect(page.getByText('My folder')).toBeVisible(); + + await page.goto('/documents'); + await expect(page.locator('div').filter({ hasText: 'My folder' }).nth(3)).toBeVisible(); +}); + +test('user can create a document subfolder inside a document folder', async ({ page }) => { + const user = await seedUser(); + + const folder = await seedBlankFolder(user, { + createFolderOptions: { + name: 'Client Contracts', + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: `/documents/f/${folder.id}`, + }); + + await expect(page.getByText('Client Contracts')).toBeVisible(); + + await page.getByRole('button', { name: 'Create Folder' }).click(); + await expect(page.getByRole('dialog', { name: 'Create New folder' })).toBeVisible(); + + await page.getByLabel('Folder name').fill('Invoices'); + await page.getByRole('button', { name: 'Create' }).click(); + + await page.waitForTimeout(1000); + + await expect(page.getByText('Invoices')).toBeVisible(); +}); + +test('user can create a document inside a document folder', async ({ page }) => { + const user = await seedUser(); + + const folder = await seedBlankFolder(user, { + createFolderOptions: { + name: 'Client Contracts', + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: `/documents/f/${folder.id}`, + }); + + const fileInput = page.locator('input[type="file"]').nth(1); + await fileInput.waitFor({ state: 'attached' }); + + await fileInput.setInputFiles( + path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf'), + ); + + await page.waitForTimeout(3000); + + await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible(); + + await page.goto(`/documents/f/${folder.id}`); + + await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible(); +}); + +test('user can pin a document folder', async ({ page }) => { + const user = await seedUser(); + + await seedBlankFolder(user, { + createFolderOptions: { + name: 'Contracts', + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/documents', + }); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Pin' }).click(); + + await page.reload(); + + await expect(page.locator('svg.text-documenso.h-3.w-3')).toBeVisible(); +}); + +test('user can unpin a document folder', async ({ page }) => { + const user = await seedUser(); + + await seedBlankFolder(user, { + createFolderOptions: { + name: 'Contracts', + pinned: true, + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/documents', + }); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Unpin' }).click(); + + await page.reload(); + + await expect(page.locator('svg.text-documenso.h-3.w-3')).not.toBeVisible(); +}); + +test('user can rename a document folder', async ({ page }) => { + const user = await seedUser(); + + await seedBlankFolder(user, { + createFolderOptions: { + name: 'Contracts', + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/documents', + }); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Settings' }).click(); + + await page.getByLabel('Name').fill('Archive'); + await page.getByRole('button', { name: 'Save Changes' }).click(); + + await expect(page.getByText('Archive')).toBeVisible(); +}); + +test('document folder visibility is not visible to user', async ({ page }) => { + const user = await seedUser(); + + await seedBlankFolder(user, { + createFolderOptions: { + name: 'Contracts', + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/documents', + }); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Settings' }).click(); + + await expect(page.getByRole('menuitem', { name: 'Visibility' })).not.toBeVisible(); +}); + +test('document folder can be moved to another document folder', async ({ page }) => { + const user = await seedUser(); + + const folder = await seedBlankFolder(user, { + createFolderOptions: { + name: 'Clients', + }, + }); + + await seedBlankFolder(user, { + createFolderOptions: { + name: 'Contracts', + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/documents', + }); + + await page.getByRole('button', { name: '•••' }).nth(0).click(); + await page.getByRole('menuitem', { name: 'Move' }).click(); + + await page.getByRole('button', { name: 'Clients' }).click(); + await page.getByRole('button', { name: 'Move Folder' }).click(); + + await page.waitForTimeout(1000); + + await page.goto(`/documents/f/${folder.id}`); + + await expect(page.getByText('Contracts')).toBeVisible(); +}); + +test('document folder can be moved to the root', async ({ page }) => { + const user = await seedUser(); + + const parentFolder = await seedBlankFolder(user, { + createFolderOptions: { + name: 'Clients', + }, + }); + + await seedBlankFolder(user, { + createFolderOptions: { + name: 'Contracts', + parentId: parentFolder.id, + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/documents', + }); + + await page.getByText('Clients').click(); + + await page.getByRole('button', { name: '•••' }).nth(0).click(); + await page.getByRole('menuitem', { name: 'Move' }).click(); + + await page.getByRole('button', { name: 'Root' }).click(); + await page.getByRole('button', { name: 'Move Folder' }).click(); + + await page.waitForTimeout(1000); + + await page.goto('/documents'); + + await expect(page.getByText('Clients')).toBeVisible(); +}); + +test('document folder and its contents can be deleted', async ({ page }) => { + const user = await seedUser(); + + const folder = await seedBlankFolder(user, { + createFolderOptions: { + name: 'Proposals', + }, + }); + + const proposal = await seedBlankDocument(user, { + createDocumentOptions: { + title: 'Proposal 1', + folderId: folder.id, + }, + }); + + const reportsFolder = await seedBlankFolder(user, { + createFolderOptions: { + name: 'Reports', + parentId: folder.id, + }, + }); + + const report = await seedBlankDocument(user, { + createDocumentOptions: { + title: 'Report 1', + folderId: reportsFolder.id, + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/documents', + }); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + + await page.getByRole('textbox').fill(`delete ${folder.name}`); + await page.getByRole('button', { name: 'Delete' }).click(); + + await page.goto('/documents'); + + await expect(page.locator('div').filter({ hasText: folder.name })).not.toBeVisible(); + await expect(page.getByText(proposal.title)).not.toBeVisible(); + + await page.goto(`/documents/f/${folder.id}`); + + await expect(page.getByText(report.title)).not.toBeVisible(); + await expect(page.locator('div').filter({ hasText: reportsFolder.name })).not.toBeVisible(); +}); + +test('user can move a document to a document folder', async ({ page }) => { + const user = await seedUser(); + + const folder = await seedBlankFolder(user, { + createFolderOptions: { + name: 'Proposals', + }, + }); + + await seedBlankDocument(user, { + createDocumentOptions: { + title: 'Proposal 1', + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/documents', + }); + + await page.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await page.getByRole('button', { name: 'Proposals' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + + await page.goto(`/documents/f/${folder.id}`); + + await expect(page.getByText('Proposal 1')).toBeVisible(); +}); + +test('user can move a document from folder to the root', async ({ page }) => { + const user = await seedUser(); + + const folder = await seedBlankFolder(user, { + createFolderOptions: { + name: 'Proposals', + }, + }); + + await seedBlankDocument(user, { + createDocumentOptions: { + title: 'Proposal 1', + folderId: folder.id, + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/documents', + }); + + await page.getByText('Proposals').click(); + + await page.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await page.getByRole('button', { name: 'Root' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + + await page.goto('/documents'); + + await expect(page.getByText('Proposal 1')).toBeVisible(); +}); + +test('create folder button is visible on templates page', async ({ page }) => { + const user = await seedUser(); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/templates', + }); + + await expect(page.getByRole('button', { name: 'Create folder' })).toBeVisible(); +}); + +test('user can create a template folder', async ({ page }) => { + const user = await seedUser(); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/templates', + }); + + await page.getByRole('button', { name: 'Create folder' }).click(); + await expect(page.getByRole('dialog', { name: 'Create New folder' })).toBeVisible(); + + await page.getByLabel('Folder name').fill('My template folder'); + await page.getByRole('button', { name: 'Create' }).click(); + + await page.waitForTimeout(1000); + + await expect(page.getByText('My template folder')).toBeVisible(); + + await page.goto('/templates'); + await expect(page.locator('div').filter({ hasText: 'My template folder' }).nth(3)).toBeVisible(); +}); + +test('user can create a template subfolder inside a template folder', async ({ page }) => { + const user = await seedUser(); + + const folder = await seedBlankFolder(user, { + createFolderOptions: { + name: 'Client Templates', + type: FolderType.TEMPLATE, + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: `/templates/f/${folder.id}`, + }); + + await expect(page.getByText('Client Templates')).toBeVisible(); + + await page.getByRole('button', { name: 'Create folder' }).click(); + await expect(page.getByRole('dialog', { name: 'Create New folder' })).toBeVisible(); + + await page.getByLabel('Folder name').fill('Contract Templates'); + await page.getByRole('button', { name: 'Create' }).click(); + + await page.waitForTimeout(1000); + + await expect(page.getByText('Contract Templates')).toBeVisible(); +}); + +test('user can create a template inside a template folder', async ({ page }) => { + const user = await seedUser(); + + const folder = await seedBlankFolder(user, { + createFolderOptions: { + name: 'Client Templates', + type: FolderType.TEMPLATE, + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: `/templates/f/${folder.id}`, + }); + + await expect(page.getByText('Client Templates')).toBeVisible(); + + await page.getByRole('button', { name: 'New Template' }).click(); + // await expect(page.getByRole('dialog', { name: 'New Template' })).toBeVisible(); + + await page + .locator('div') + .filter({ hasText: /^Upload Template DocumentDrag & drop your PDF here\.$/ }) + .nth(2) + .click(); + await page.locator('input[type="file"]').waitFor({ state: 'attached' }); + + await page + .locator('input[type="file"]') + .setInputFiles(path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf')); + + await page.waitForTimeout(3000); + + await page.getByRole('button', { name: 'Create' }).click(); + + await page.waitForTimeout(1000); + + await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible(); + + await page.goto(`/templates/f/${folder.id}`); + await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible(); +}); + +test('user can pin a template folder', async ({ page }) => { + const user = await seedUser(); + + await seedBlankFolder(user, { + createFolderOptions: { + name: 'Contract Templates', + type: FolderType.TEMPLATE, + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/templates', + }); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Pin' }).click(); + + await page.reload(); + + await expect(page.locator('svg.text-documenso.h-3.w-3')).toBeVisible(); +}); + +test('user can unpin a template folder', async ({ page }) => { + const user = await seedUser(); + + await seedBlankFolder(user, { + createFolderOptions: { + name: 'Contract Templates', + pinned: true, + type: FolderType.TEMPLATE, + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/templates', + }); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Unpin' }).click(); + + await page.reload(); + + await expect(page.locator('svg.text-documenso.h-3.w-3')).not.toBeVisible(); +}); + +test('user can rename a template folder', async ({ page }) => { + const user = await seedUser(); + + await seedBlankFolder(user, { + createFolderOptions: { + name: 'Contract Templates', + type: FolderType.TEMPLATE, + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/templates', + }); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Settings' }).click(); + + await page.getByLabel('Name').fill('Updated Template Folder'); + await page.getByRole('button', { name: 'Save Changes' }).click(); + + await expect(page.getByText('Updated Template Folder')).toBeVisible(); +}); + +test('template folder visibility is not visible to user', async ({ page }) => { + const user = await seedUser(); + + await seedBlankFolder(user, { + createFolderOptions: { + name: 'Contract Templates', + type: FolderType.TEMPLATE, + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/templates', + }); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Settings' }).click(); + + await expect(page.getByRole('menuitem', { name: 'Visibility' })).not.toBeVisible(); +}); + +test('template folder can be moved to another template folder', async ({ page }) => { + const user = await seedUser(); + + const folder = await seedBlankFolder(user, { + createFolderOptions: { + name: 'Client Templates', + type: FolderType.TEMPLATE, + }, + }); + + await seedBlankFolder(user, { + createFolderOptions: { + name: 'Contract Templates', + type: FolderType.TEMPLATE, + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/templates', + }); + + await page.getByRole('button', { name: '•••' }).nth(0).click(); + await page.getByRole('menuitem', { name: 'Move' }).click(); + + await page.getByRole('button', { name: 'Client Templates' }).click(); + await page.getByRole('button', { name: 'Move Folder' }).click(); + + await page.waitForTimeout(1000); + + await page.goto(`/templates/f/${folder.id}`); + + await expect(page.getByText('Contract Templates')).toBeVisible(); +}); + +test('template folder can be moved to the root', async ({ page }) => { + const user = await seedUser(); + + const parentFolder = await seedBlankFolder(user, { + createFolderOptions: { + name: 'Client Templates', + type: FolderType.TEMPLATE, + }, + }); + + await seedBlankFolder(user, { + createFolderOptions: { + name: 'Contract Templates', + parentId: parentFolder.id, + type: FolderType.TEMPLATE, + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/templates', + }); + + await page.getByText('Client Templates').click(); + await page.getByRole('button', { name: '•••' }).nth(0).click(); + await page.getByRole('menuitem', { name: 'Move' }).click(); + + await page.getByRole('button', { name: 'Root' }).click(); + await page.getByRole('button', { name: 'Move Folder' }).click(); + + await page.waitForTimeout(1000); + + await page.goto('/templates'); + + await expect(page.getByText('Contract Templates')).toBeVisible(); +}); + +test('template folder and its contents can be deleted', async ({ page }) => { + const user = await seedUser(); + + const folder = await seedBlankFolder(user, { + createFolderOptions: { + name: 'Proposal Templates', + type: FolderType.TEMPLATE, + }, + }); + + const template = await seedBlankTemplate(user, { + createTemplateOptions: { + title: 'Proposal Template 1', + folderId: folder.id, + }, + }); + + const subfolder = await seedBlankFolder(user, { + createFolderOptions: { + name: 'Report Templates', + parentId: folder.id, + type: FolderType.TEMPLATE, + }, + }); + + const reportTemplate = await seedBlankTemplate(user, { + createTemplateOptions: { + title: 'Report Template 1', + folderId: subfolder.id, + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/templates', + }); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + + await page.getByRole('textbox').fill(`delete ${folder.name}`); + await page.getByRole('button', { name: 'Delete' }).click(); + + await page.goto('/templates'); + + await expect(page.locator('div').filter({ hasText: folder.name })).not.toBeVisible(); + await expect(page.getByText(template.title)).not.toBeVisible(); + + await page.goto(`/templates/f/${folder.id}`); + + await expect(page.getByText(reportTemplate.title)).not.toBeVisible(); + await expect(page.locator('div').filter({ hasText: subfolder.name })).not.toBeVisible(); +}); + +test('user can navigate between template folders', async ({ page }) => { + const user = await seedUser(); + + const parentFolder = await seedBlankFolder(user, { + createFolderOptions: { + name: 'Client Templates', + type: FolderType.TEMPLATE, + }, + }); + + const subfolder = await seedBlankFolder(user, { + createFolderOptions: { + name: 'Contract Templates', + parentId: parentFolder.id, + type: FolderType.TEMPLATE, + }, + }); + + await seedBlankTemplate(user, { + createTemplateOptions: { + title: 'Contract Template 1', + folderId: subfolder.id, + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/templates', + }); + + await page.getByText('Client Templates').click(); + await expect(page.getByText('Contract Templates')).toBeVisible(); + + await page.getByText('Contract Templates').click(); + await expect(page.getByText('Contract Template 1')).toBeVisible(); + + await page.getByRole('button', { name: parentFolder.name }).click(); + await expect(page.getByText('Contract Templates')).toBeVisible(); + + await page.getByRole('button', { name: subfolder.name }).click(); + await expect(page.getByText('Contract Template 1')).toBeVisible(); +}); + +test('user can move a template to a template folder', async ({ page }) => { + const user = await seedUser(); + + const folder = await seedBlankFolder(user, { + createFolderOptions: { + name: 'Client Templates', + type: FolderType.TEMPLATE, + }, + }); + + await seedBlankTemplate(user, { + createTemplateOptions: { + title: 'Proposal Template 1', + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/templates', + }); + + await page.getByTestId('template-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await page.getByRole('button', { name: 'Client Templates' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.goto(`/templates/f/${folder.id}`); + await page.waitForTimeout(1000); + + await expect(page.getByText('Proposal Template 1')).toBeVisible(); +}); + +test('user can move a template from a folder to the root', async ({ page }) => { + const user = await seedUser(); + + const folder = await seedBlankFolder(user, { + createFolderOptions: { + name: 'Client Templates', + type: FolderType.TEMPLATE, + }, + }); + + await seedBlankTemplate(user, { + createTemplateOptions: { + title: 'Proposal Template 1', + folderId: folder.id, + }, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: '/templates', + }); + + await page.getByText('Client Templates').click(); + + await page.getByTestId('template-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await page.getByRole('button', { name: 'Root' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + + await page.goto('/templates'); + + await expect(page.getByText('Proposal Template 1')).toBeVisible(); +}); diff --git a/packages/app-tests/e2e/folders/team-account-folders.spec.ts b/packages/app-tests/e2e/folders/team-account-folders.spec.ts new file mode 100644 index 000000000..e908adfd3 --- /dev/null +++ b/packages/app-tests/e2e/folders/team-account-folders.spec.ts @@ -0,0 +1,2873 @@ +import { expect, test } from '@playwright/test'; +import path from 'node:path'; + +import { prisma } from '@documenso/prisma'; +import { DocumentVisibility, FolderType, TeamMemberRole } from '@documenso/prisma/client'; +import { seedTeamDocuments } from '@documenso/prisma/seed/documents'; +import { seedBlankDocument } from '@documenso/prisma/seed/documents'; +import { seedBlankFolder } from '@documenso/prisma/seed/folders'; +import { seedTeamMember } from '@documenso/prisma/seed/teams'; +import { seedTeam } from '@documenso/prisma/seed/teams'; +import { seedBlankTemplate } from '@documenso/prisma/seed/templates'; + +import { apiSignin } from '../fixtures/authentication'; + +test.describe.configure({ mode: 'parallel' }); + +test('[TEAMS]: create document folder button is visible', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}`, + }); + + await expect(page.getByRole('button', { name: 'Create Folder' })).toBeVisible(); +}); + +test('[TEAMS]: can create document folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}`, + }); + + await page.getByRole('button', { name: 'Create Folder' }).click(); + + await page.getByLabel('Folder name').fill('Team Folder'); + await page.getByRole('button', { name: 'Create' }).click(); + + await expect(page.getByText('Team Folder')).toBeVisible(); +}); + +test('[TEAMS]: can create document subfolder within a document folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}`, + }); + + const teamFolder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Folder', + teamId: team.id, + }, + }); + + await page.goto(`/t/${team.url}/documents/f/${teamFolder.id}`); + + await page.getByRole('button', { name: 'Create Folder' }).click(); + + await page.getByLabel('Folder name').fill('Subfolder'); + await page.getByRole('button', { name: 'Create' }).click(); + + await expect(page.getByText('Subfolder')).toBeVisible(); +}); + +test('[TEAMS]: can create a document inside a document folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamFolder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Documents', + teamId: team.id, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents/f/${teamFolder.id}`, + }); + + const fileInput = page.locator('input[type="file"]').nth(1); + await fileInput.waitFor({ state: 'attached' }); + + await fileInput.setInputFiles( + path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf'), + ); + + await page.waitForTimeout(3000); + + await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents/f/${teamFolder.id}`); + + await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible(); +}); + +test('[TEAMS]: can pin a document folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Contracts', + teamId: team.id, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Pin' }).click(); + + await page.reload(); + + await expect(page.locator('svg.text-documenso.h-3.w-3')).toBeVisible(); +}); + +test('[TEAMS]: can unpin a document folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Contracts', + pinned: true, + teamId: team.id, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Unpin' }).click(); + + await page.reload(); + + await expect(page.locator('svg.text-documenso.h-3.w-3')).not.toBeVisible(); +}); + +test('[TEAMS]: can rename a document folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Contracts', + teamId: team.id, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Settings' }).click(); + + await page.getByLabel('Name').fill('Team Archive'); + await page.getByRole('button', { name: 'Save Changes' }).click(); + + await expect(page.getByText('Team Archive')).toBeVisible(); +}); + +test('[TEAMS]: document folder visibility is visible to team member', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Contracts', + teamId: team.id, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Settings' }).click(); + + await expect(page.getByRole('combobox', { name: 'Visibility' })).toBeVisible(); +}); + +test('[TEAMS]: document folder can be moved to another document folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const folder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Clients', + teamId: team.id, + }, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Contracts', + teamId: team.id, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await page.getByRole('button', { name: '•••' }).nth(0).click(); + await page.getByRole('menuitem', { name: 'Move' }).click(); + + await page.getByRole('button', { name: 'Team Clients' }).click(); + await page.getByRole('button', { name: 'Move Folder' }).click(); + + await page.waitForTimeout(1000); + + await page.goto(`/t/${team.url}/documents/f/${folder.id}`); + + await expect(page.getByText('Team Contracts')).toBeVisible(); +}); + +test('[TEAMS]: document folder and its contents can be deleted', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const folder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Proposals', + teamId: team.id, + }, + }); + + const proposal = await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Team Proposal 1', + folderId: folder.id, + }, + }); + + const reportsFolder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Reports', + parentId: folder.id, + teamId: team.id, + }, + }); + + const report = await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Team Report 1', + folderId: reportsFolder.id, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + + await page.getByRole('textbox').fill(`delete ${folder.name}`); + await page.getByRole('button', { name: 'Delete' }).click(); + + await page.goto(`/t/${team.url}/documents`); + + await expect(page.locator('div').filter({ hasText: folder.name })).not.toBeVisible(); + await expect(page.getByText(proposal.title)).not.toBeVisible(); + + await page.goto(`/t/${team.url}/documents/f/${folder.id}`); + + await expect(page.getByText(report.title)).not.toBeVisible(); + await expect(page.locator('div').filter({ hasText: reportsFolder.name })).not.toBeVisible(); +}); + +test('[TEAMS]: create folder button is visible on templates page', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/templates`, + }); + + await expect(page.getByRole('button', { name: 'Create Folder' })).toBeVisible(); +}); + +test('[TEAMS]: can create a template folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/templates`, + }); + + await page.getByRole('button', { name: 'Create Folder' }).click(); + await expect(page.getByRole('dialog', { name: 'Create New folder' })).toBeVisible(); + + await page.getByLabel('Folder name').fill('Team template folder'); + await page.getByRole('button', { name: 'Create' }).click(); + + await page.waitForTimeout(1000); + + await expect(page.getByText('Team template folder')).toBeVisible(); + + await page.goto(`/t/${team.url}/templates`); + await expect( + page.locator('div').filter({ hasText: 'Team template folder' }).nth(3), + ).toBeVisible(); +}); + +test('[TEAMS]: can create a template subfolder inside a template folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const folder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Client Templates', + type: FolderType.TEMPLATE, + teamId: team.id, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/templates/f/${folder.id}`, + }); + + await expect(page.getByText('Team Client Templates')).toBeVisible(); + + await page.getByRole('button', { name: 'Create Folder' }).click(); + await expect(page.getByRole('dialog', { name: 'Create New folder' })).toBeVisible(); + + await page.getByLabel('Folder name').fill('Team Contract Templates'); + await page.getByRole('button', { name: 'Create' }).click(); + + await page.waitForTimeout(1000); + + await expect(page.getByText('Team Contract Templates')).toBeVisible(); +}); + +test('[TEAMS]: can create a template inside a template folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const folder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Client Templates', + type: FolderType.TEMPLATE, + teamId: team.id, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/templates/f/${folder.id}`, + }); + + await expect(page.getByText('Team Client Templates')).toBeVisible(); + + await page.getByRole('button', { name: 'New Template' }).click(); + + await page + .locator('div') + .filter({ hasText: /^Upload Template DocumentDrag & drop your PDF here\.$/ }) + .nth(2) + .click(); + await page.locator('input[type="file"]').waitFor({ state: 'attached' }); + + await page + .locator('input[type="file"]') + .setInputFiles(path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf')); + + await page.waitForTimeout(3000); + + await page.getByRole('button', { name: 'Create' }).click(); + + await page.waitForTimeout(1000); + + await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible(); + + await page.goto(`/t/${team.url}/templates/f/${folder.id}`); + await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible(); +}); + +test('[TEAMS]: can pin a template folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Contract Templates', + type: FolderType.TEMPLATE, + teamId: team.id, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/templates`, + }); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Pin' }).click(); + + await page.reload(); + + await expect(page.locator('svg.text-documenso.h-3.w-3')).toBeVisible(); +}); + +test('[TEAMS]: can unpin a template folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Contract Templates', + pinned: true, + type: FolderType.TEMPLATE, + teamId: team.id, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/templates`, + }); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Unpin' }).click(); + + await page.reload(); + await page.waitForTimeout(1000); + + await expect(page.locator('svg.text-documenso.h-3.w-3')).not.toBeVisible(); +}); + +test('[TEAMS]: can rename a template folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Contract Templates', + type: FolderType.TEMPLATE, + teamId: team.id, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/templates`, + }); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Settings' }).click(); + + await page.getByLabel('Name').fill('Updated Team Template Folder'); + await page.getByRole('button', { name: 'Save Changes' }).click(); + + await expect(page.getByText('Updated Team Template Folder')).toBeVisible(); +}); + +test('[TEAMS]: template folder visibility is not visible to team member', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Contract Templates', + type: FolderType.TEMPLATE, + teamId: team.id, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/templates`, + }); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Settings' }).click(); + + await expect(page.getByRole('menuitem', { name: 'Visibility' })).not.toBeVisible(); +}); + +test('[TEAMS]: template folder can be moved to another template folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const folder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Client Templates', + type: FolderType.TEMPLATE, + teamId: team.id, + }, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Contract Templates', + type: FolderType.TEMPLATE, + teamId: team.id, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/templates`, + }); + + await page.getByRole('button', { name: '•••' }).nth(0).click(); + await page.getByRole('menuitem', { name: 'Move' }).click(); + + await page.getByRole('button', { name: 'Team Client Templates' }).click(); + await page.getByRole('button', { name: 'Move Folder' }).click(); + + await page.waitForTimeout(1000); + + await page.goto(`/t/${team.url}/templates/f/${folder.id}`); + + await expect(page.getByText('Team Contract Templates')).toBeVisible(); +}); + +test('[TEAMS]: template folder and its contents can be deleted', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const folder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Proposal Templates', + type: FolderType.TEMPLATE, + teamId: team.id, + }, + }); + + const template = await seedBlankTemplate(team.owner, { + createTemplateOptions: { + title: 'Team Proposal Template 1', + folderId: folder.id, + }, + }); + + const subfolder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Report Templates', + parentId: folder.id, + type: FolderType.TEMPLATE, + teamId: team.id, + }, + }); + + const reportTemplate = await seedBlankTemplate(team.owner, { + createTemplateOptions: { + title: 'Team Report Template 1', + folderId: subfolder.id, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/templates`, + }); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + + await page.getByRole('textbox').fill(`delete ${folder.name}`); + await page.getByRole('button', { name: 'Delete' }).click(); + + await page.goto(`/t/${team.url}/templates`); + + await expect(page.locator('div').filter({ hasText: folder.name })).not.toBeVisible(); + await expect(page.getByText(template.title)).not.toBeVisible(); + + await page.goto(`/t/${team.url}/templates/f/${folder.id}`); + + await expect(page.getByText(reportTemplate.title)).not.toBeVisible(); + await expect(page.locator('div').filter({ hasText: subfolder.name })).not.toBeVisible(); +}); + +test('[TEAMS]: can navigate between template folders', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const parentFolder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Client Templates', + type: FolderType.TEMPLATE, + teamId: team.id, + }, + }); + + const subfolder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Contract Templates', + parentId: parentFolder.id, + type: FolderType.TEMPLATE, + teamId: team.id, + }, + }); + + await seedBlankTemplate(team.owner, { + createTemplateOptions: { + title: 'Team Contract Template 1', + folderId: subfolder.id, + teamId: team.id, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/templates`, + }); + + await page.getByText('Team Client Templates').click(); + await expect(page.getByText('Team Contract Templates')).toBeVisible(); + + await page.getByText('Team Contract Templates').click(); + await expect(page.getByText('Team Contract Template 1')).toBeVisible(); + + await page.getByRole('button', { name: parentFolder.name }).click(); + await expect(page.getByText('Team Contract Templates')).toBeVisible(); + + await page.getByRole('button', { name: subfolder.name }).click(); + await expect(page.getByText('Team Contract Template 1')).toBeVisible(); +}); + +test('[TEAMS]: folder visibility is properly applied based on team member roles', async ({ + page, +}) => { + const { team } = await seedTeamDocuments(); + + const teamMember1 = await seedTeamMember({ + teamId: team.id, + name: 'Team Member 1', + role: TeamMemberRole.MEMBER, + }); + + const teamMember2 = await seedTeamMember({ + teamId: team.id, + name: 'Team Member 2', + role: TeamMemberRole.MANAGER, + }); + + const teamMember3 = await seedTeamMember({ + teamId: team.id, + name: 'Team Member 3', + role: TeamMemberRole.ADMIN, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Admin Only Folder', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Manager Folder', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Everyone Folder', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Admin Only Folder')).toBeVisible(); + await expect(page.getByText('Manager Folder')).toBeVisible(); + await expect(page.getByText('Everyone Folder')).toBeVisible(); + + await apiSignin({ + page, + email: teamMember1.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Admin Only Folder')).not.toBeVisible(); + await expect(page.getByText('Manager Folder')).not.toBeVisible(); + await expect(page.getByText('Everyone Folder')).toBeVisible(); + + await apiSignin({ + page, + email: teamMember2.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Admin Only Folder')).not.toBeVisible(); + await expect(page.getByText('Manager Folder')).toBeVisible(); + await expect(page.getByText('Everyone Folder')).toBeVisible(); + + await apiSignin({ + page, + email: teamMember3.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Admin Only Folder')).toBeVisible(); + await expect(page.getByText('Manager Folder')).toBeVisible(); + await expect(page.getByText('Everyone Folder')).toBeVisible(); +}); + +test('[TEAMS]: folder inherits team visibility settings', async ({ page }) => { + const team = await seedTeam(); + + await prisma.teamGlobalSettings.create({ + data: { + teamId: team.id, + documentVisibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await page.getByRole('button', { name: 'Create Folder' }).click(); + await page.getByLabel('Name').fill('Admin Only Folder'); + await page.getByRole('button', { name: 'Create' }).click(); + + await expect(page.getByText('Admin Only Folder')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents/`); + + await page.getByRole('button', { name: '•••' }).click(); + await page.getByRole('menuitem', { name: 'Settings' }).click(); + + await expect(page.getByRole('combobox', { name: 'Visibility' })).toHaveText('Admins only'); + + await prisma.teamGlobalSettings.update({ + where: { teamId: team.id }, + data: { documentVisibility: DocumentVisibility.MANAGER_AND_ABOVE }, + }); + + await page.reload(); + + await page.getByRole('button', { name: 'Create Folder' }).click(); + await page.getByLabel('Name').fill('Manager and above Folder'); + await page.getByRole('button', { name: 'Create' }).click(); + + await expect(page.getByText('Manager and above Folder')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents/`); + + await page.getByRole('button', { name: '•••' }).nth(0).click(); + await page.getByRole('menuitem', { name: 'Settings' }).click(); + + await expect(page.getByRole('combobox', { name: 'Visibility' })).toHaveText('Managers and above'); + + await prisma.teamGlobalSettings.update({ + where: { teamId: team.id }, + data: { documentVisibility: DocumentVisibility.EVERYONE }, + }); + + await page.reload(); + + await page.getByRole('button', { name: 'Create Folder' }).click(); + await page.getByLabel('Name').fill('Everyone Folder'); + await page.getByRole('button', { name: 'Create' }).click(); + + await expect(page.getByText('Everyone Folder')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents/`); + + await page.getByRole('button', { name: '•••' }).nth(0).click(); + await page.getByRole('menuitem', { name: 'Settings' }).click(); + + await expect(page.getByRole('combobox', { name: 'Visibility' })).toHaveText('Everyone'); +}); + +test('[TEAMS]: documents inherit folder visibility', async ({ page }) => { + const team = await seedTeam(); + + await prisma.teamGlobalSettings.create({ + data: { + teamId: team.id, + documentVisibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await page.getByRole('button', { name: 'Create Folder' }).click(); + await page.getByLabel('Name').fill('Admin Only Folder'); + await page.getByRole('button', { name: 'Create' }).click(); + + await expect(page.getByText('Admin Only Folder')).toBeVisible(); + + await page.getByText('Admin Only Folder').click(); + + const fileInput = page.locator('input[type="file"]').nth(1); + await fileInput.waitFor({ state: 'attached' }); + + await fileInput.setInputFiles( + path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf'), + ); + + await page.waitForTimeout(3000); + + await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible(); + + await expect(page.getByRole('combobox').filter({ hasText: 'Admins only' })).toBeVisible(); +}); + +test('[TEAMS]: documents are properly organized within folders', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const folder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Team Folder', + teamId: team.id, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Folder Document', + folderId: folder.id, + teamId: team.id, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Folder Document')).not.toBeVisible(); + + await page.getByText('Team Folder').click(); + await page.waitForTimeout(1000); + + await expect(page.getByText('Folder Document')).toBeVisible(); +}); + +test('[TEAMS]: team member can move documents to everyone folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamMember = await seedTeamMember({ + teamId: team.id, + name: 'Team Member', + role: TeamMemberRole.MEMBER, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Admin Only Folder', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Manager Folder', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Everyone Folder', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Admin Document', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Manager Document', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Everyone Document', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await apiSignin({ + page, + email: teamMember.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Admin Only Folder')).not.toBeVisible(); + await expect(page.getByText('Manager Folder')).not.toBeVisible(); + await expect(page.getByText('Everyone Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Admin Document')).not.toBeVisible(); + await expect(page.getByText('[TEST] Manager Document')).not.toBeVisible(); + await expect(page.getByText('[TEST] Everyone Document')).toBeVisible(); + + const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ }); + await everyoneDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move' }).click(); + + await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Everyone Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(3000); + await expect(page.getByText('[TEST] Everyone Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(3000); + + await expect(page.getByText('[TEST] Everyone Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team manager can move manager document to manager folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamManager = await seedTeamMember({ + teamId: team.id, + name: 'Team Manager', + role: TeamMemberRole.MANAGER, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Manager Folder', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Manager Document', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await apiSignin({ + page, + email: teamManager.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Manager Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Manager Document')).toBeVisible(); + + const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ }); + await managerDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Manager Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + + await expect(page.getByText('[TEST] Manager Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + + await expect(page.getByText('[TEST] Manager Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team manager can move manager document to everyone folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamManager = await seedTeamMember({ + teamId: team.id, + name: 'Team Manager', + role: TeamMemberRole.MANAGER, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Everyone Folder', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Manager Document', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await apiSignin({ + page, + email: teamManager.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Everyone Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Manager Document')).toBeVisible(); + + const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ }); + await managerDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Everyone Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + + await expect(page.getByText('[TEST] Manager Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + + await expect(page.getByText('[TEST] Manager Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team manager can move everyone document to manager folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamManager = await seedTeamMember({ + teamId: team.id, + name: 'Team Manager', + role: TeamMemberRole.MANAGER, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Manager Folder', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Everyone Document', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await apiSignin({ + page, + email: teamManager.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Manager Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Everyone Document')).toBeVisible(); + + const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ }); + await everyoneDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Manager Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + + await expect(page.getByText('[TEST] Everyone Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + + await expect(page.getByText('[TEST] Everyone Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team admin can move admin document to admin folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamAdmin = await seedTeamMember({ + teamId: team.id, + name: 'Team Admin', + role: TeamMemberRole.ADMIN, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Admin Folder', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Admin Document', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: teamAdmin.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Admin Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Admin Document')).toBeVisible(); + + const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ }); + await adminDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Admin Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Admin Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Admin Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team admin can move admin document to manager folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamAdmin = await seedTeamMember({ + teamId: team.id, + name: 'Team Admin', + role: TeamMemberRole.ADMIN, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Manager Folder', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Admin Document', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: teamAdmin.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Manager Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Admin Document')).toBeVisible(); + + const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ }); + await adminDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Manager Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Admin Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Admin Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team admin can move admin document to everyone folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamAdmin = await seedTeamMember({ + teamId: team.id, + name: 'Team Admin', + role: TeamMemberRole.ADMIN, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Everyone Folder', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Admin Document', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: teamAdmin.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Everyone Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Admin Document')).toBeVisible(); + + const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ }); + await adminDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Everyone Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Admin Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Admin Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team admin can move manager document to admin folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamAdmin = await seedTeamMember({ + teamId: team.id, + name: 'Team Admin', + role: TeamMemberRole.ADMIN, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Admin Folder', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Manager Document', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await apiSignin({ + page, + email: teamAdmin.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Admin Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Manager Document')).toBeVisible(); + + const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ }); + await managerDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Admin Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Manager Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Manager Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team admin can move manager document to manager folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamAdmin = await seedTeamMember({ + teamId: team.id, + name: 'Team Admin', + role: TeamMemberRole.ADMIN, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Manager Folder', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Manager Document', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await apiSignin({ + page, + email: teamAdmin.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Manager Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Manager Document')).toBeVisible(); + + const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ }); + await managerDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Manager Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Manager Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Manager Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team admin can move manager document to everyone folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamAdmin = await seedTeamMember({ + teamId: team.id, + name: 'Team Admin', + role: TeamMemberRole.ADMIN, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Everyone Folder', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Manager Document', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await apiSignin({ + page, + email: teamAdmin.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Everyone Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Manager Document')).toBeVisible(); + + const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ }); + await managerDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Everyone Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Manager Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Manager Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team admin can move everyone document to admin folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamAdmin = await seedTeamMember({ + teamId: team.id, + name: 'Team Admin', + role: TeamMemberRole.ADMIN, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Admin Folder', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Everyone Document', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await apiSignin({ + page, + email: teamAdmin.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Admin Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Everyone Document')).toBeVisible(); + + const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ }); + await everyoneDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Admin Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Everyone Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Everyone Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team admin can move everyone document to manager folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamAdmin = await seedTeamMember({ + teamId: team.id, + name: 'Team Admin', + role: TeamMemberRole.ADMIN, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Manager Folder', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Everyone Document', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await apiSignin({ + page, + email: teamAdmin.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Manager Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Everyone Document')).toBeVisible(); + + const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ }); + await everyoneDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Manager Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Everyone Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Everyone Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team admin can move everyone document to everyone folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamAdmin = await seedTeamMember({ + teamId: team.id, + name: 'Team Admin', + role: TeamMemberRole.ADMIN, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Everyone Folder', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Everyone Document', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await apiSignin({ + page, + email: teamAdmin.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Everyone Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Everyone Document')).toBeVisible(); + + const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ }); + await everyoneDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Everyone Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Everyone Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Everyone Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team owner can move admin document to admin folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Admin Folder', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Admin Document', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Admin Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Admin Document')).toBeVisible(); + + const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ }); + await adminDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Admin Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Admin Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Admin Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team owner can move admin document to manager folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Manager Folder', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Admin Document', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Manager Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Admin Document')).toBeVisible(); + + const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ }); + await adminDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Manager Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Admin Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Admin Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team owner can move admin document to everyone folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Everyone Folder', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Admin Document', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Everyone Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Admin Document')).toBeVisible(); + + const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ }); + await adminDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Everyone Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Admin Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Admin Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team owner can move manager document to admin folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Admin Folder', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Manager Document', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Admin Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Manager Document')).toBeVisible(); + + const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ }); + await managerDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Admin Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Manager Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Manager Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team owner can move manager document to manager folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Manager Folder', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Manager Document', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Manager Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Manager Document')).toBeVisible(); + + const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ }); + await managerDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Manager Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Manager Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Manager Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team owner can move manager document to everyone folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Everyone Folder', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Manager Document', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Everyone Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Manager Document')).toBeVisible(); + + const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ }); + await managerDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Everyone Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Manager Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Manager Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team owner can move everyone document to admin folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Admin Folder', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Everyone Document', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Admin Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Everyone Document')).toBeVisible(); + + const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ }); + await everyoneDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Admin Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Everyone Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Everyone Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team owner can move everyone document to manager folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Manager Folder', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Everyone Document', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Manager Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Everyone Document')).toBeVisible(); + + const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ }); + await everyoneDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Manager Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Everyone Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Everyone Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team owner can move everyone document to everyone folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Everyone Folder', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + type: FolderType.DOCUMENT, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: '[TEST] Everyone Document', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Everyone Folder')).toBeVisible(); + await expect(page.getByText('[TEST] Everyone Document')).toBeVisible(); + + const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ }); + await everyoneDocRow.getByTestId('document-table-action-btn').click(); + await page.getByRole('menuitem', { name: 'Move to Folder' }).click(); + + await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible(); + await page.getByRole('button', { name: 'Everyone Folder' }).click(); + await page.getByRole('button', { name: 'Move' }).click(); + + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Everyone Document')).toBeVisible(); + + await page.goto(`/t/${team.url}/documents`); + await page.waitForTimeout(1000); + await expect(page.getByText('[TEST] Everyone Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team member cannot see admin folder in folder list', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamMember = await seedTeamMember({ + teamId: team.id, + name: 'Team Member', + role: TeamMemberRole.MEMBER, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Admin Only Folder', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: teamMember.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Admin Only Folder')).not.toBeVisible(); +}); + +test('[TEAMS]: team member can access admin folder via URL and see everyone documents', async ({ + page, +}) => { + const { team } = await seedTeamDocuments(); + + const teamMember = await seedTeamMember({ + teamId: team.id, + name: 'Team Member', + role: TeamMemberRole.MEMBER, + }); + + const adminFolder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Admin Only Folder', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Admin Folder - Everyone Document', + folderId: adminFolder.id, + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Admin Folder - Manager Document', + folderId: adminFolder.id, + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Admin Folder - Admin Document', + folderId: adminFolder.id, + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: teamMember.email, + redirectPath: `/t/${team.url}/documents/f/${adminFolder.id}`, + }); + + await expect(page.getByRole('button', { name: 'Admin Only Folder' })).not.toBeVisible(); + await expect(page.getByText('Admin Folder - Everyone Document')).toBeVisible(); + await expect(page.getByText('Admin Folder - Manager Document')).not.toBeVisible(); + await expect(page.getByText('Admin Folder - Admin Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team member cannot see manager folder in folder list', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamMember = await seedTeamMember({ + teamId: team.id, + name: 'Team Member', + role: TeamMemberRole.MEMBER, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Manager Folder', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await apiSignin({ + page, + email: teamMember.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Manager Folder')).not.toBeVisible(); +}); + +test('[TEAMS]: team member can access manager folder via URL and see everyone documents', async ({ + page, +}) => { + const { team } = await seedTeamDocuments(); + + const teamMember = await seedTeamMember({ + teamId: team.id, + name: 'Team Member', + role: TeamMemberRole.MEMBER, + }); + + const managerFolder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Manager Folder', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Manager Folder - Everyone Document', + folderId: managerFolder.id, + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Manager Folder - Manager Document', + folderId: managerFolder.id, + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Manager Folder - Admin Document', + folderId: managerFolder.id, + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: teamMember.email, + redirectPath: `/t/${team.url}/documents/f/${managerFolder.id}`, + }); + + await expect(page.getByRole('button', { name: 'Manager Folder' })).not.toBeVisible(); + await expect(page.getByText('Manager Folder - Everyone Document')).toBeVisible(); + await expect(page.getByText('Manager Folder - Manager Document')).not.toBeVisible(); + await expect(page.getByText('Manager Folder - Admin Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team member can see everyone folders', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamMember = await seedTeamMember({ + teamId: team.id, + name: 'Team Member', + role: TeamMemberRole.MEMBER, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Admin Only Folder', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Manager Folder', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Everyone Folder', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await apiSignin({ + page, + email: teamMember.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Admin Only Folder')).not.toBeVisible(); + await expect(page.getByText('Manager Folder')).not.toBeVisible(); + await expect(page.getByText('Everyone Folder')).toBeVisible(); +}); + +test('[TEAMS]: team member can only see everyone documents in everyone folder', async ({ + page, +}) => { + const { team } = await seedTeamDocuments(); + + const teamMember = await seedTeamMember({ + teamId: team.id, + name: 'Team Member', + role: TeamMemberRole.MEMBER, + }); + + const everyoneFolder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Everyone Folder', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Everyone Document', + folderId: everyoneFolder.id, + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Manager Document', + folderId: everyoneFolder.id, + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Admin Document', + folderId: everyoneFolder.id, + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: teamMember.email, + redirectPath: `/t/${team.url}/documents/f/${everyoneFolder.id}`, + }); + + await expect(page.getByText('Everyone Document')).toBeVisible(); + await expect(page.getByText('Manager Document')).not.toBeVisible(); + await expect(page.getByText('Admin Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team manager can see manager and everyone folders in folder list', async ({ + page, +}) => { + const { team } = await seedTeamDocuments(); + + const teamManager = await seedTeamMember({ + teamId: team.id, + name: 'Team Manager', + role: TeamMemberRole.MANAGER, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Admin Only Folder', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Manager Folder', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Everyone Folder', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await apiSignin({ + page, + email: teamManager.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Admin Only Folder')).not.toBeVisible(); + await expect(page.getByText('Manager Folder')).toBeVisible(); + await expect(page.getByText('Everyone Folder')).toBeVisible(); +}); + +test('[TEAMS]: team manager can see manager and everyone documents in manager folder', async ({ + page, +}) => { + const { team } = await seedTeamDocuments(); + + const teamManager = await seedTeamMember({ + teamId: team.id, + name: 'Team Manager', + role: TeamMemberRole.MANAGER, + }); + + const managerFolder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Manager Folder', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Manager Folder - Everyone Document', + folderId: managerFolder.id, + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Manager Folder - Manager Document', + folderId: managerFolder.id, + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Manager Folder - Admin Document', + folderId: managerFolder.id, + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: teamManager.email, + redirectPath: `/t/${team.url}/documents/f/${managerFolder.id}`, + }); + + await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible(); + await expect(page.getByText('Manager Folder - Everyone Document')).toBeVisible(); + await expect(page.getByText('Manager Folder - Manager Document')).toBeVisible(); + await expect(page.getByText('Manager Folder - Admin Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team manager can see manager and everyone documents in everyone folder', async ({ + page, +}) => { + const { team } = await seedTeamDocuments(); + + const teamManager = await seedTeamMember({ + teamId: team.id, + name: 'Team Manager', + role: TeamMemberRole.MANAGER, + }); + + const everyoneFolder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Everyone Folder', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Everyone Folder - Everyone Document', + folderId: everyoneFolder.id, + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Everyone Folder - Manager Document', + folderId: everyoneFolder.id, + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Everyone Folder - Admin Document', + folderId: everyoneFolder.id, + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: teamManager.email, + redirectPath: `/t/${team.url}/documents/f/${everyoneFolder.id}`, + }); + + await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible(); + await expect(page.getByText('Everyone Folder - Everyone Document')).toBeVisible(); + await expect(page.getByText('Everyone Folder - Manager Document')).toBeVisible(); + await expect(page.getByText('Everyone Folder - Admin Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team manager can access admin folder via URL and see manager and everyone documents', async ({ + page, +}) => { + const { team } = await seedTeamDocuments(); + + const teamManager = await seedTeamMember({ + teamId: team.id, + name: 'Team Manager', + role: TeamMemberRole.MANAGER, + }); + + const adminFolder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Admin Only Folder', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Admin Folder - Everyone Document', + folderId: adminFolder.id, + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Admin Folder - Manager Document', + folderId: adminFolder.id, + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Admin Folder - Admin Document', + folderId: adminFolder.id, + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: teamManager.email, + redirectPath: `/t/${team.url}/documents/f/${adminFolder.id}`, + }); + + await expect(page.getByRole('button', { name: 'Admin Only Folder' })).not.toBeVisible(); + await expect(page.getByText('Admin Folder - Everyone Document')).toBeVisible(); + await expect(page.getByText('Admin Folder - Manager Document')).toBeVisible(); + await expect(page.getByText('Admin Folder - Admin Document')).not.toBeVisible(); +}); + +test('[TEAMS]: team owner can see all folders in folder list', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Admin Only Folder', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Manager Folder', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Everyone Folder', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Admin Only Folder')).toBeVisible(); + await expect(page.getByText('Manager Folder')).toBeVisible(); + await expect(page.getByText('Everyone Folder')).toBeVisible(); +}); + +test('[TEAMS]: team owner can see all documents in admin folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const adminFolder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Admin Only Folder', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Admin Folder - Everyone Document', + folderId: adminFolder.id, + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Admin Folder - Manager Document', + folderId: adminFolder.id, + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Admin Folder - Admin Document', + folderId: adminFolder.id, + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents/f/${adminFolder.id}`, + }); + + await expect(page.getByRole('button', { name: 'Admin Only Folder' })).toBeVisible(); + await expect(page.getByText('Admin Folder - Everyone Document')).toBeVisible(); + await expect(page.getByText('Admin Folder - Manager Document')).toBeVisible(); + await expect(page.getByText('Admin Folder - Admin Document')).toBeVisible(); +}); + +test('[TEAMS]: team owner can see all documents in manager folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const managerFolder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Manager Folder', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Manager Folder - Everyone Document', + folderId: managerFolder.id, + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Manager Folder - Manager Document', + folderId: managerFolder.id, + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Manager Folder - Admin Document', + folderId: managerFolder.id, + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents/f/${managerFolder.id}`, + }); + + await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible(); + await expect(page.getByText('Manager Folder - Everyone Document')).toBeVisible(); + await expect(page.getByText('Manager Folder - Manager Document')).toBeVisible(); + await expect(page.getByText('Manager Folder - Admin Document')).toBeVisible(); +}); + +test('[TEAMS]: team owner can see all documents in everyone folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const everyoneFolder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Everyone Folder', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Everyone Folder - Everyone Document', + folderId: everyoneFolder.id, + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Everyone Folder - Manager Document', + folderId: everyoneFolder.id, + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Everyone Folder - Admin Document', + folderId: everyoneFolder.id, + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: team.owner.email, + redirectPath: `/t/${team.url}/documents/f/${everyoneFolder.id}`, + }); + + await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible(); + await expect(page.getByText('Everyone Folder - Everyone Document')).toBeVisible(); + await expect(page.getByText('Everyone Folder - Manager Document')).toBeVisible(); + await expect(page.getByText('Everyone Folder - Admin Document')).toBeVisible(); +}); + +test('[TEAMS]: team admin can see all folders in folder list', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamAdmin = await seedTeamMember({ + teamId: team.id, + name: 'Team Admin', + role: TeamMemberRole.ADMIN, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Admin Only Folder', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Manager Folder', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Everyone Folder', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await apiSignin({ + page, + email: teamAdmin.email, + redirectPath: `/t/${team.url}/documents`, + }); + + await expect(page.getByText('Admin Only Folder')).toBeVisible(); + await expect(page.getByText('Manager Folder')).toBeVisible(); + await expect(page.getByText('Everyone Folder')).toBeVisible(); +}); + +test('[TEAMS]: team admin can see all documents in admin folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamAdmin = await seedTeamMember({ + teamId: team.id, + name: 'Team Admin', + role: TeamMemberRole.ADMIN, + }); + + const adminFolder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Admin Only Folder', + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Admin Folder - Everyone Document', + folderId: adminFolder.id, + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Admin Folder - Manager Document', + folderId: adminFolder.id, + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Admin Folder - Admin Document', + folderId: adminFolder.id, + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: teamAdmin.email, + redirectPath: `/t/${team.url}/documents/f/${adminFolder.id}`, + }); + + await expect(page.getByRole('button', { name: 'Admin Only Folder' })).toBeVisible(); + await expect(page.getByText('Admin Folder - Everyone Document')).toBeVisible(); + await expect(page.getByText('Admin Folder - Manager Document')).toBeVisible(); + await expect(page.getByText('Admin Folder - Admin Document')).toBeVisible(); +}); + +test('[TEAMS]: team admin can see all documents in manager folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamAdmin = await seedTeamMember({ + teamId: team.id, + name: 'Team Admin', + role: TeamMemberRole.ADMIN, + }); + + const managerFolder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Manager Folder', + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Manager Folder - Everyone Document', + folderId: managerFolder.id, + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Manager Folder - Manager Document', + folderId: managerFolder.id, + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Manager Folder - Admin Document', + folderId: managerFolder.id, + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: teamAdmin.email, + redirectPath: `/t/${team.url}/documents/f/${managerFolder.id}`, + }); + + await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible(); + await expect(page.getByText('Manager Folder - Everyone Document')).toBeVisible(); + await expect(page.getByText('Manager Folder - Manager Document')).toBeVisible(); + await expect(page.getByText('Manager Folder - Admin Document')).toBeVisible(); +}); + +test('[TEAMS]: team admin can see all documents in everyone folder', async ({ page }) => { + const { team } = await seedTeamDocuments(); + + const teamAdmin = await seedTeamMember({ + teamId: team.id, + name: 'Team Admin', + role: TeamMemberRole.ADMIN, + }); + + const everyoneFolder = await seedBlankFolder(team.owner, { + createFolderOptions: { + name: 'Everyone Folder', + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Everyone Folder - Everyone Document', + folderId: everyoneFolder.id, + teamId: team.id, + visibility: DocumentVisibility.EVERYONE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Everyone Folder - Manager Document', + folderId: everyoneFolder.id, + teamId: team.id, + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }); + + await seedBlankDocument(team.owner, { + createDocumentOptions: { + title: 'Everyone Folder - Admin Document', + folderId: everyoneFolder.id, + teamId: team.id, + visibility: DocumentVisibility.ADMIN, + }, + }); + + await apiSignin({ + page, + email: teamAdmin.email, + redirectPath: `/t/${team.url}/documents/f/${everyoneFolder.id}`, + }); + + await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible(); + await expect(page.getByText('Everyone Folder - Everyone Document')).toBeVisible(); + await expect(page.getByText('Everyone Folder - Manager Document')).toBeVisible(); + await expect(page.getByText('Everyone Folder - Admin Document')).toBeVisible(); +}); diff --git a/packages/app-tests/package.json b/packages/app-tests/package.json index 516a8f844..eff48e063 100644 --- a/packages/app-tests/package.json +++ b/packages/app-tests/package.json @@ -21,4 +21,4 @@ "dependencies": { "start-server-and-test": "^2.0.1" } -} \ No newline at end of file +} diff --git a/packages/lib/server-only/document/create-document-v2.ts b/packages/lib/server-only/document/create-document-v2.ts index 77babbba4..a87860488 100644 --- a/packages/lib/server-only/document/create-document-v2.ts +++ b/packages/lib/server-only/document/create-document-v2.ts @@ -233,6 +233,7 @@ export const createDocumentV2 = async ({ documentMeta: true, recipients: true, fields: true, + folder: true, }, }); diff --git a/packages/lib/server-only/document/create-document.ts b/packages/lib/server-only/document/create-document.ts index 8ca98ca29..866b0e2f9 100644 --- a/packages/lib/server-only/document/create-document.ts +++ b/packages/lib/server-only/document/create-document.ts @@ -1,5 +1,5 @@ import { DocumentSource, WebhookTriggerEvents } from '@prisma/client'; -import type { Team, TeamGlobalSettings } from '@prisma/client'; +import type { DocumentVisibility, Team, TeamGlobalSettings } from '@prisma/client'; import { TeamMemberRole } from '@prisma/client'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; @@ -29,6 +29,7 @@ export type CreateDocumentOptions = { normalizePdf?: boolean; timezone?: string; requestMetadata: ApiRequestMetadata; + folderId?: string; }; export const createDocument = async ({ @@ -41,6 +42,7 @@ export const createDocument = async ({ formValues, requestMetadata, timezone, + folderId, }: CreateDocumentOptions) => { const user = await prisma.user.findFirstOrThrow({ where: { @@ -89,6 +91,29 @@ export const createDocument = async ({ userTeamRole = teamWithUserRole.members[0]?.role; } + let folderVisibility: DocumentVisibility | undefined; + + if (folderId) { + const folder = await prisma.folder.findFirst({ + where: { + id: folderId, + userId, + teamId, + }, + select: { + visibility: true, + }, + }); + + if (!folder) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Folder not found', + }); + } + + folderVisibility = folder.visibility; + } + if (normalizePdf) { const documentData = await prisma.documentData.findFirst({ where: { @@ -121,10 +146,13 @@ export const createDocument = async ({ documentDataId, userId, teamId, - visibility: determineDocumentVisibility( - team?.teamGlobalSettings?.documentVisibility, - userTeamRole ?? TeamMemberRole.MEMBER, - ), + folderId, + visibility: + folderVisibility ?? + determineDocumentVisibility( + team?.teamGlobalSettings?.documentVisibility, + userTeamRole ?? TeamMemberRole.MEMBER, + ), formValues, source: DocumentSource.DOCUMENT, documentMeta: { diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index a7a689ee0..cd5694d65 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -27,6 +27,7 @@ export type FindDocumentsOptions = { period?: PeriodSelectorValue; senderIds?: number[]; query?: string; + folderId?: string; }; export const findDocuments = async ({ @@ -41,6 +42,7 @@ export const findDocuments = async ({ period, senderIds, query = '', + folderId, }: FindDocumentsOptions) => { const user = await prisma.user.findFirstOrThrow({ where: { @@ -120,10 +122,10 @@ export const findDocuments = async ({ }, ]; - let filters: Prisma.DocumentWhereInput | null = findDocumentsFilter(status, user); + let filters: Prisma.DocumentWhereInput | null = findDocumentsFilter(status, user, folderId); if (team) { - filters = findTeamDocumentsFilter(status, team, visibilityFilters); + filters = findTeamDocumentsFilter(status, team, visibilityFilters, folderId); } if (filters === null) { @@ -227,6 +229,12 @@ export const findDocuments = async ({ }; } + if (folderId !== undefined) { + whereClause.folderId = folderId; + } else { + whereClause.folderId = null; + } + const [data, count] = await Promise.all([ prisma.document.findMany({ where: whereClause, @@ -273,13 +281,18 @@ export const findDocuments = async ({ } satisfies FindResultResponse; }; -const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => { +const findDocumentsFilter = ( + status: ExtendedDocumentStatus, + user: User, + folderId?: string | null, +) => { return match(status) .with(ExtendedDocumentStatus.ALL, () => ({ OR: [ { userId: user.id, teamId: null, + folderId: folderId, }, { status: ExtendedDocumentStatus.COMPLETED, @@ -288,6 +301,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => { email: user.email, }, }, + folderId: folderId, }, { status: ExtendedDocumentStatus.PENDING, @@ -296,6 +310,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => { email: user.email, }, }, + folderId: folderId, }, ], })) @@ -324,6 +339,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => { userId: user.id, teamId: null, status: ExtendedDocumentStatus.PENDING, + folderId: folderId, }, { status: ExtendedDocumentStatus.PENDING, @@ -336,6 +352,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => { }, }, }, + folderId: folderId, }, ], })) @@ -345,6 +362,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => { userId: user.id, teamId: null, status: ExtendedDocumentStatus.COMPLETED, + folderId: folderId, }, { status: ExtendedDocumentStatus.COMPLETED, @@ -353,6 +371,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => { email: user.email, }, }, + folderId: folderId, }, ], })) @@ -362,6 +381,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => { userId: user.id, teamId: null, status: ExtendedDocumentStatus.REJECTED, + folderId: folderId, }, { status: ExtendedDocumentStatus.REJECTED, @@ -371,6 +391,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => { signingStatus: SigningStatus.REJECTED, }, }, + folderId: folderId, }, ], })) @@ -410,6 +431,7 @@ const findTeamDocumentsFilter = ( status: ExtendedDocumentStatus, team: Team & { teamEmail: TeamEmail | null }, visibilityFilters: Prisma.DocumentWhereInput[], + folderId?: string, ) => { const teamEmail = team.teamEmail?.email ?? null; @@ -420,6 +442,7 @@ const findTeamDocumentsFilter = ( OR: [ { teamId: team.id, + folderId: folderId, OR: visibilityFilters, }, ], @@ -437,6 +460,7 @@ const findTeamDocumentsFilter = ( }, }, OR: visibilityFilters, + folderId: folderId, }); // Filter to display all documents that have been sent by the team email. @@ -445,6 +469,7 @@ const findTeamDocumentsFilter = ( email: teamEmail, }, OR: visibilityFilters, + folderId: folderId, }); } @@ -470,6 +495,7 @@ const findTeamDocumentsFilter = ( }, }, OR: visibilityFilters, + folderId: folderId, }; }) .with(ExtendedDocumentStatus.DRAFT, () => { @@ -479,6 +505,7 @@ const findTeamDocumentsFilter = ( teamId: team.id, status: ExtendedDocumentStatus.DRAFT, OR: visibilityFilters, + folderId: folderId, }, ], }; @@ -490,6 +517,7 @@ const findTeamDocumentsFilter = ( email: teamEmail, }, OR: visibilityFilters, + folderId: folderId, }); } @@ -502,6 +530,7 @@ const findTeamDocumentsFilter = ( teamId: team.id, status: ExtendedDocumentStatus.PENDING, OR: visibilityFilters, + folderId: folderId, }, ], }; @@ -521,12 +550,14 @@ const findTeamDocumentsFilter = ( }, }, OR: visibilityFilters, + folderId: folderId, }, { user: { email: teamEmail, }, OR: visibilityFilters, + folderId: folderId, }, ], }); diff --git a/packages/lib/server-only/document/get-document-by-id.ts b/packages/lib/server-only/document/get-document-by-id.ts index 5d06a7405..8925a6915 100644 --- a/packages/lib/server-only/document/get-document-by-id.ts +++ b/packages/lib/server-only/document/get-document-by-id.ts @@ -12,9 +12,15 @@ export type GetDocumentByIdOptions = { documentId: number; userId: number; teamId?: number; + folderId?: string; }; -export const getDocumentById = async ({ documentId, userId, teamId }: GetDocumentByIdOptions) => { +export const getDocumentById = async ({ + documentId, + userId, + teamId, + folderId, +}: GetDocumentByIdOptions) => { const documentWhereInput = await getDocumentWhereInput({ documentId, userId, @@ -22,7 +28,10 @@ export const getDocumentById = async ({ documentId, userId, teamId }: GetDocumen }); const document = await prisma.document.findFirst({ - where: documentWhereInput, + where: { + ...documentWhereInput, + folderId, + }, include: { documentData: true, documentMeta: true, diff --git a/packages/lib/server-only/document/get-document-with-details-by-id.ts b/packages/lib/server-only/document/get-document-with-details-by-id.ts index 9cabb6838..1898ca29b 100644 --- a/packages/lib/server-only/document/get-document-with-details-by-id.ts +++ b/packages/lib/server-only/document/get-document-with-details-by-id.ts @@ -7,12 +7,14 @@ export type GetDocumentWithDetailsByIdOptions = { documentId: number; userId: number; teamId?: number; + folderId?: string; }; export const getDocumentWithDetailsById = async ({ documentId, userId, teamId, + folderId, }: GetDocumentWithDetailsByIdOptions) => { const documentWhereInput = await getDocumentWhereInput({ documentId, @@ -21,12 +23,16 @@ export const getDocumentWithDetailsById = async ({ }); const document = await prisma.document.findFirst({ - where: documentWhereInput, + where: { + ...documentWhereInput, + folderId, + }, include: { documentData: true, documentMeta: true, recipients: true, fields: true, + folder: true, }, }); diff --git a/packages/lib/server-only/document/get-stats.ts b/packages/lib/server-only/document/get-stats.ts index 7b12f72c5..754da367a 100644 --- a/packages/lib/server-only/document/get-stats.ts +++ b/packages/lib/server-only/document/get-stats.ts @@ -15,9 +15,16 @@ export type GetStatsInput = { team?: Omit; period?: PeriodSelectorValue; search?: string; + folderId?: string; }; -export const getStats = async ({ user, period, search = '', ...options }: GetStatsInput) => { +export const getStats = async ({ + user, + period, + search = '', + folderId, + ...options +}: GetStatsInput) => { let createdAt: Prisma.DocumentWhereInput['createdAt']; if (period) { @@ -37,8 +44,9 @@ export const getStats = async ({ user, period, search = '', ...options }: GetSta currentUserEmail: user.email, userId: user.id, search, + folderId, }) - : getCounts({ user, createdAt, search })); + : getCounts({ user, createdAt, search, folderId })); const stats: Record = { [ExtendedDocumentStatus.DRAFT]: 0, @@ -84,9 +92,10 @@ type GetCountsOption = { user: User; createdAt: Prisma.DocumentWhereInput['createdAt']; search?: string; + folderId?: string | null; }; -const getCounts = async ({ user, createdAt, search }: GetCountsOption) => { +const getCounts = async ({ user, createdAt, search, folderId }: GetCountsOption) => { const searchFilter: Prisma.DocumentWhereInput = { OR: [ { title: { contains: search, mode: 'insensitive' } }, @@ -95,6 +104,8 @@ const getCounts = async ({ user, createdAt, search }: GetCountsOption) => { ], }; + const rootPageFilter = folderId === undefined ? { folderId: null } : {}; + return Promise.all([ // Owner counts. prisma.document.groupBy({ @@ -107,7 +118,7 @@ const getCounts = async ({ user, createdAt, search }: GetCountsOption) => { createdAt, teamId: null, deletedAt: null, - AND: [searchFilter], + AND: [searchFilter, rootPageFilter, folderId ? { folderId } : {}], }, }), // Not signed counts. @@ -126,7 +137,7 @@ const getCounts = async ({ user, createdAt, search }: GetCountsOption) => { }, }, createdAt, - AND: [searchFilter], + AND: [searchFilter, rootPageFilter, folderId ? { folderId } : {}], }, }), // Has signed counts. @@ -164,7 +175,7 @@ const getCounts = async ({ user, createdAt, search }: GetCountsOption) => { }, }, ], - AND: [searchFilter], + AND: [searchFilter, rootPageFilter, folderId ? { folderId } : {}], }, }), ]); @@ -179,10 +190,11 @@ type GetTeamCountsOption = { createdAt: Prisma.DocumentWhereInput['createdAt']; currentTeamMemberRole?: TeamMemberRole; search?: string; + folderId?: string | null; }; const getTeamCounts = async (options: GetTeamCountsOption) => { - const { createdAt, teamId, teamEmail } = options; + const { createdAt, teamId, teamEmail, folderId } = options; const senderIds = options.senderIds ?? []; @@ -206,6 +218,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => { createdAt, teamId, deletedAt: null, + folderId, }; let notSignedCountsGroupByArgs = null; @@ -278,6 +291,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => { where: { userId: userIdWhereClause, createdAt, + folderId, status: ExtendedDocumentStatus.PENDING, recipients: { some: { @@ -298,6 +312,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => { where: { userId: userIdWhereClause, createdAt, + folderId, OR: [ { status: ExtendedDocumentStatus.PENDING, diff --git a/packages/lib/server-only/folder/create-folder.ts b/packages/lib/server-only/folder/create-folder.ts new file mode 100644 index 000000000..a92b986de --- /dev/null +++ b/packages/lib/server-only/folder/create-folder.ts @@ -0,0 +1,86 @@ +import { TeamMemberRole } from '@prisma/client'; +import type { Team, TeamGlobalSettings } from '@prisma/client'; + +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { prisma } from '@documenso/prisma'; + +import type { TFolderType } from '../../types/folder-type'; +import { FolderType } from '../../types/folder-type'; +import { determineDocumentVisibility } from '../../utils/document-visibility'; + +export interface CreateFolderOptions { + userId: number; + teamId?: number; + name: string; + parentId?: string | null; + type?: TFolderType; +} + +export const createFolder = async ({ + userId, + teamId, + name, + parentId, + type = FolderType.DOCUMENT, +}: CreateFolderOptions) => { + const user = await prisma.user.findFirstOrThrow({ + where: { + id: userId, + }, + include: { + teamMembers: { + select: { + teamId: true, + }, + }, + }, + }); + + if ( + teamId !== undefined && + !user.teamMembers.some((teamMember) => teamMember.teamId === teamId) + ) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Team not found', + }); + } + + let team: (Team & { teamGlobalSettings: TeamGlobalSettings | null }) | null = null; + let userTeamRole: TeamMemberRole | undefined; + + if (teamId) { + const teamWithUserRole = await prisma.team.findFirstOrThrow({ + where: { + id: teamId, + }, + include: { + teamGlobalSettings: true, + members: { + where: { + userId: userId, + }, + select: { + role: true, + }, + }, + }, + }); + + team = teamWithUserRole; + userTeamRole = teamWithUserRole.members[0]?.role; + } + + return await prisma.folder.create({ + data: { + name, + userId, + teamId, + parentId, + type, + visibility: determineDocumentVisibility( + team?.teamGlobalSettings?.documentVisibility, + userTeamRole ?? TeamMemberRole.MEMBER, + ), + }, + }); +}; diff --git a/packages/lib/server-only/folder/delete-folder.ts b/packages/lib/server-only/folder/delete-folder.ts new file mode 100644 index 000000000..76ac8a62b --- /dev/null +++ b/packages/lib/server-only/folder/delete-folder.ts @@ -0,0 +1,85 @@ +import { DocumentVisibility, TeamMemberRole } from '@prisma/client'; +import { match } from 'ts-pattern'; + +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { prisma } from '@documenso/prisma'; + +export interface DeleteFolderOptions { + userId: number; + teamId?: number; + folderId: string; +} + +export const deleteFolder = async ({ userId, teamId, folderId }: DeleteFolderOptions) => { + let teamMemberRole: TeamMemberRole | null = null; + + if (teamId) { + const team = await prisma.team.findFirst({ + where: { + id: teamId, + members: { + some: { + userId, + }, + }, + }, + include: { + members: { + where: { + userId, + }, + select: { + role: true, + }, + }, + }, + }); + + if (!team) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Team not found', + }); + } + + teamMemberRole = team.members[0]?.role ?? null; + } + + const folder = await prisma.folder.findFirst({ + where: { + id: folderId, + userId, + teamId, + }, + include: { + documents: true, + subfolders: true, + templates: true, + }, + }); + + if (!folder) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Folder not found', + }); + } + + if (teamId && teamMemberRole) { + const hasPermission = match(teamMemberRole) + .with(TeamMemberRole.ADMIN, () => true) + .with(TeamMemberRole.MANAGER, () => folder.visibility !== DocumentVisibility.ADMIN) + .with(TeamMemberRole.MEMBER, () => folder.visibility === DocumentVisibility.EVERYONE) + .otherwise(() => false); + + if (!hasPermission) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'You do not have permission to delete this folder', + }); + } + } + + return await prisma.folder.delete({ + where: { + id: folderId, + }, + }); +}; diff --git a/packages/lib/server-only/folder/find-folders.ts b/packages/lib/server-only/folder/find-folders.ts new file mode 100644 index 000000000..ec4df30d3 --- /dev/null +++ b/packages/lib/server-only/folder/find-folders.ts @@ -0,0 +1,152 @@ +import { TeamMemberRole } from '@prisma/client'; +import { match } from 'ts-pattern'; + +import { prisma } from '@documenso/prisma'; + +import { DocumentVisibility } from '../../types/document-visibility'; +import type { TFolderType } from '../../types/folder-type'; + +export interface FindFoldersOptions { + userId: number; + teamId?: number; + parentId?: string | null; + type?: TFolderType; +} + +export const findFolders = async ({ userId, teamId, parentId, type }: FindFoldersOptions) => { + let team = null; + let teamMemberRole = null; + + if (teamId !== undefined) { + try { + team = await prisma.team.findFirstOrThrow({ + where: { + id: teamId, + members: { + some: { + userId, + }, + }, + }, + include: { + members: { + where: { + userId, + }, + select: { + role: true, + }, + }, + }, + }); + + teamMemberRole = team.members[0].role; + } catch (error) { + console.error('Error finding team:', error); + throw error; + } + } + + const visibilityFilters = match(teamMemberRole) + .with(TeamMemberRole.ADMIN, () => ({ + visibility: { + in: [ + DocumentVisibility.EVERYONE, + DocumentVisibility.MANAGER_AND_ABOVE, + DocumentVisibility.ADMIN, + ], + }, + })) + .with(TeamMemberRole.MANAGER, () => ({ + visibility: { + in: [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE], + }, + })) + .otherwise(() => ({ visibility: DocumentVisibility.EVERYONE })); + + const whereClause = { + AND: [ + { parentId }, + teamId + ? { + OR: [ + { teamId, ...visibilityFilters }, + { userId, teamId }, + ], + } + : { userId, teamId: null }, + ], + }; + + try { + const folders = await prisma.folder.findMany({ + where: { + ...whereClause, + ...(type ? { type } : {}), + }, + orderBy: [{ pinned: 'desc' }, { createdAt: 'desc' }], + }); + + const foldersWithDetails = await Promise.all( + folders.map(async (folder) => { + try { + const [subfolders, documentCount, templateCount, subfolderCount] = await Promise.all([ + prisma.folder.findMany({ + where: { + parentId: folder.id, + ...(teamId ? { teamId, ...visibilityFilters } : { userId, teamId: null }), + }, + orderBy: { + createdAt: 'desc', + }, + }), + prisma.document.count({ + where: { + folderId: folder.id, + }, + }), + prisma.template.count({ + where: { + folderId: folder.id, + }, + }), + prisma.folder.count({ + where: { + parentId: folder.id, + ...(teamId ? { teamId, ...visibilityFilters } : { userId, teamId: null }), + }, + }), + ]); + + const subfoldersWithEmptySubfolders = subfolders.map((subfolder) => ({ + ...subfolder, + subfolders: [], + _count: { + documents: 0, + templates: 0, + subfolders: 0, + }, + })); + + return { + ...folder, + subfolders: subfoldersWithEmptySubfolders, + _count: { + documents: documentCount, + templates: templateCount, + subfolders: subfolderCount, + }, + }; + } catch (error) { + console.error('Error processing folder:', folder.id, error); + throw error; + } + }), + ); + + return foldersWithDetails; + } catch (error) { + console.error('Error in findFolders:', error); + throw error; + } +}; diff --git a/packages/lib/server-only/folder/get-folder-breadcrumbs.ts b/packages/lib/server-only/folder/get-folder-breadcrumbs.ts new file mode 100644 index 000000000..c5d7327b0 --- /dev/null +++ b/packages/lib/server-only/folder/get-folder-breadcrumbs.ts @@ -0,0 +1,112 @@ +import { TeamMemberRole } from '@prisma/client'; +import { match } from 'ts-pattern'; + +import { prisma } from '@documenso/prisma'; + +import { DocumentVisibility } from '../../types/document-visibility'; +import type { TFolderType } from '../../types/folder-type'; + +export interface GetFolderBreadcrumbsOptions { + userId: number; + teamId?: number; + folderId: string; + type?: TFolderType; +} + +export const getFolderBreadcrumbs = async ({ + userId, + teamId, + folderId, + type, +}: GetFolderBreadcrumbsOptions) => { + let teamMemberRole = null; + + if (teamId !== undefined) { + try { + const team = await prisma.team.findFirstOrThrow({ + where: { + id: teamId, + members: { + some: { + userId, + }, + }, + }, + include: { + members: { + where: { + userId, + }, + select: { + role: true, + }, + }, + }, + }); + + teamMemberRole = team.members[0].role; + } catch (error) { + console.error('Error finding team:', error); + return []; + } + } + + const visibilityFilters = match(teamMemberRole) + .with(TeamMemberRole.ADMIN, () => ({ + visibility: { + in: [ + DocumentVisibility.EVERYONE, + DocumentVisibility.MANAGER_AND_ABOVE, + DocumentVisibility.ADMIN, + ], + }, + })) + .with(TeamMemberRole.MANAGER, () => ({ + visibility: { + in: [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE], + }, + })) + .otherwise(() => ({ visibility: DocumentVisibility.EVERYONE })); + + const whereClause = (folderId: string) => ({ + id: folderId, + ...(type ? { type } : {}), + ...(teamId + ? { + OR: [ + { teamId, ...visibilityFilters }, + { userId, teamId }, + ], + } + : { userId, teamId: null }), + }); + + const breadcrumbs = []; + let currentFolderId = folderId; + + const currentFolder = await prisma.folder.findFirst({ + where: whereClause(currentFolderId), + }); + + if (!currentFolder) { + return []; + } + + breadcrumbs.push(currentFolder); + + while (currentFolder?.parentId) { + const parentFolder = await prisma.folder.findFirst({ + where: whereClause(currentFolder.parentId), + }); + + if (!parentFolder) { + break; + } + + breadcrumbs.unshift(parentFolder); + currentFolderId = parentFolder.id; + currentFolder.parentId = parentFolder.parentId; + } + + return breadcrumbs; +}; diff --git a/packages/lib/server-only/folder/get-folder-by-id.ts b/packages/lib/server-only/folder/get-folder-by-id.ts new file mode 100644 index 000000000..c4a069f90 --- /dev/null +++ b/packages/lib/server-only/folder/get-folder-by-id.ts @@ -0,0 +1,92 @@ +import { TeamMemberRole } from '@prisma/client'; +import { match } from 'ts-pattern'; + +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { prisma } from '@documenso/prisma'; + +import { DocumentVisibility } from '../../types/document-visibility'; +import type { TFolderType } from '../../types/folder-type'; + +export interface GetFolderByIdOptions { + userId: number; + teamId?: number; + folderId?: string; + type?: TFolderType; +} + +export const getFolderById = async ({ userId, teamId, folderId, type }: GetFolderByIdOptions) => { + let teamMemberRole = null; + + if (teamId !== undefined) { + try { + const team = await prisma.team.findFirstOrThrow({ + where: { + id: teamId, + members: { + some: { + userId, + }, + }, + }, + include: { + members: { + where: { + userId, + }, + select: { + role: true, + }, + }, + }, + }); + + teamMemberRole = team.members[0].role; + } catch (error) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Team not found', + }); + } + } + + const visibilityFilters = match(teamMemberRole) + .with(TeamMemberRole.ADMIN, () => ({ + visibility: { + in: [ + DocumentVisibility.EVERYONE, + DocumentVisibility.MANAGER_AND_ABOVE, + DocumentVisibility.ADMIN, + ], + }, + })) + .with(TeamMemberRole.MANAGER, () => ({ + visibility: { + in: [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE], + }, + })) + .otherwise(() => ({ visibility: DocumentVisibility.EVERYONE })); + + const whereClause = { + id: folderId, + ...(type ? { type } : {}), + ...(teamId + ? { + OR: [ + { teamId, ...visibilityFilters }, + { userId, teamId }, + ], + } + : { userId, teamId: null }), + }; + + const folder = await prisma.folder.findFirst({ + where: whereClause, + }); + + if (!folder) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Folder not found', + }); + } + + return folder; +}; diff --git a/packages/lib/server-only/folder/move-document-to-folder.ts b/packages/lib/server-only/folder/move-document-to-folder.ts new file mode 100644 index 000000000..d0ecb3188 --- /dev/null +++ b/packages/lib/server-only/folder/move-document-to-folder.ts @@ -0,0 +1,130 @@ +import { TeamMemberRole } from '@prisma/client'; +import { match } from 'ts-pattern'; + +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { DocumentVisibility } from '@documenso/lib/types/document-visibility'; +import { FolderType } from '@documenso/lib/types/folder-type'; +import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; +import { prisma } from '@documenso/prisma'; + +export interface MoveDocumentToFolderOptions { + userId: number; + teamId?: number; + documentId: number; + folderId?: string | null; + requestMetadata?: ApiRequestMetadata; +} + +export const moveDocumentToFolder = async ({ + userId, + teamId, + documentId, + folderId, +}: MoveDocumentToFolderOptions) => { + let teamMemberRole = null; + + if (teamId !== undefined) { + try { + const team = await prisma.team.findFirstOrThrow({ + where: { + id: teamId, + members: { + some: { + userId, + }, + }, + }, + include: { + members: { + where: { + userId, + }, + select: { + role: true, + }, + }, + }, + }); + + teamMemberRole = team.members[0].role; + } catch (error) { + console.error('Error finding team:', error); + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Team not found', + }); + } + } + + const visibilityFilters = match(teamMemberRole) + .with(TeamMemberRole.ADMIN, () => ({ + visibility: { + in: [ + DocumentVisibility.EVERYONE, + DocumentVisibility.MANAGER_AND_ABOVE, + DocumentVisibility.ADMIN, + ], + }, + })) + .with(TeamMemberRole.MANAGER, () => ({ + visibility: { + in: [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE], + }, + })) + .otherwise(() => ({ visibility: DocumentVisibility.EVERYONE })); + + const documentWhereClause = { + id: documentId, + ...(teamId + ? { + OR: [ + { teamId, ...visibilityFilters }, + { userId, teamId }, + ], + } + : { userId, teamId: null }), + }; + + const document = await prisma.document.findFirst({ + where: documentWhereClause, + }); + + if (!document) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Document not found', + }); + } + + if (folderId) { + const folderWhereClause = { + id: folderId, + type: FolderType.DOCUMENT, + ...(teamId + ? { + OR: [ + { teamId, ...visibilityFilters }, + { userId, teamId }, + ], + } + : { userId, teamId: null }), + }; + + const folder = await prisma.folder.findFirst({ + where: folderWhereClause, + }); + + if (!folder) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Folder not found', + }); + } + } + + return await prisma.document.update({ + where: { + id: documentId, + }, + data: { + folderId, + }, + }); +}; diff --git a/packages/lib/server-only/folder/move-folder.ts b/packages/lib/server-only/folder/move-folder.ts new file mode 100644 index 000000000..84372a5d7 --- /dev/null +++ b/packages/lib/server-only/folder/move-folder.ts @@ -0,0 +1,85 @@ +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; +import { prisma } from '@documenso/prisma'; + +export interface MoveFolderOptions { + userId: number; + teamId?: number; + folderId?: string; + parentId?: string | null; + requestMetadata?: ApiRequestMetadata; +} + +export const moveFolder = async ({ userId, teamId, folderId, parentId }: MoveFolderOptions) => { + return await prisma.$transaction(async (tx) => { + const folder = await tx.folder.findFirst({ + where: { + id: folderId, + userId, + teamId, + }, + }); + + if (!folder) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Folder not found', + }); + } + + if (parentId) { + const parentFolder = await tx.folder.findFirst({ + where: { + id: parentId, + userId, + teamId, + type: folder.type, + }, + }); + + if (!parentFolder) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Parent folder not found', + }); + } + + if (parentId === folderId) { + throw new AppError(AppErrorCode.INVALID_REQUEST, { + message: 'Cannot move a folder into itself', + }); + } + + let currentParentId = parentFolder.parentId; + while (currentParentId) { + if (currentParentId === folderId) { + throw new AppError(AppErrorCode.INVALID_REQUEST, { + message: 'Cannot move a folder into its descendant', + }); + } + + const currentParent = await tx.folder.findUnique({ + where: { + id: currentParentId, + }, + select: { + parentId: true, + }, + }); + + if (!currentParent) { + break; + } + + currentParentId = currentParent.parentId; + } + } + + return await tx.folder.update({ + where: { + id: folderId, + }, + data: { + parentId, + }, + }); + }); +}; diff --git a/packages/lib/server-only/folder/move-template-to-folder.ts b/packages/lib/server-only/folder/move-template-to-folder.ts new file mode 100644 index 000000000..f826fac3e --- /dev/null +++ b/packages/lib/server-only/folder/move-template-to-folder.ts @@ -0,0 +1,59 @@ +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { FolderType } from '@documenso/lib/types/folder-type'; +import { prisma } from '@documenso/prisma'; + +export interface MoveTemplateToFolderOptions { + userId: number; + teamId?: number; + templateId: number; + folderId?: string | null; +} + +export const moveTemplateToFolder = async ({ + userId, + teamId, + templateId, + folderId, +}: MoveTemplateToFolderOptions) => { + return await prisma.$transaction(async (tx) => { + const template = await tx.template.findFirst({ + where: { + id: templateId, + userId, + teamId, + }, + }); + + if (!template) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Template not found', + }); + } + + if (folderId !== null) { + const folder = await tx.folder.findFirst({ + where: { + id: folderId, + userId, + teamId, + type: FolderType.TEMPLATE, + }, + }); + + if (!folder) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Folder not found', + }); + } + } + + return await tx.template.update({ + where: { + id: templateId, + }, + data: { + folderId, + }, + }); + }); +}; diff --git a/packages/lib/server-only/folder/pin-folder.ts b/packages/lib/server-only/folder/pin-folder.ts new file mode 100644 index 000000000..5ef38ece6 --- /dev/null +++ b/packages/lib/server-only/folder/pin-folder.ts @@ -0,0 +1,37 @@ +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { prisma } from '@documenso/prisma'; + +import type { TFolderType } from '../../types/folder-type'; + +export interface PinFolderOptions { + userId: number; + teamId?: number; + folderId: string; + type?: TFolderType; +} + +export const pinFolder = async ({ userId, teamId, folderId, type }: PinFolderOptions) => { + const folder = await prisma.folder.findFirst({ + where: { + id: folderId, + userId, + teamId, + type, + }, + }); + + if (!folder) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Folder not found', + }); + } + + return await prisma.folder.update({ + where: { + id: folderId, + }, + data: { + pinned: true, + }, + }); +}; diff --git a/packages/lib/server-only/folder/unpin-folder.ts b/packages/lib/server-only/folder/unpin-folder.ts new file mode 100644 index 000000000..b7a244bed --- /dev/null +++ b/packages/lib/server-only/folder/unpin-folder.ts @@ -0,0 +1,37 @@ +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { prisma } from '@documenso/prisma'; + +import type { TFolderType } from '../../types/folder-type'; + +export interface UnpinFolderOptions { + userId: number; + teamId?: number; + folderId: string; + type?: TFolderType; +} + +export const unpinFolder = async ({ userId, teamId, folderId, type }: UnpinFolderOptions) => { + const folder = await prisma.folder.findFirst({ + where: { + id: folderId, + userId, + teamId, + type, + }, + }); + + if (!folder) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Folder not found', + }); + } + + return await prisma.folder.update({ + where: { + id: folderId, + }, + data: { + pinned: false, + }, + }); +}; diff --git a/packages/lib/server-only/folder/update-folder.ts b/packages/lib/server-only/folder/update-folder.ts new file mode 100644 index 000000000..064047752 --- /dev/null +++ b/packages/lib/server-only/folder/update-folder.ts @@ -0,0 +1,53 @@ +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { prisma } from '@documenso/prisma'; +import { DocumentVisibility } from '@documenso/prisma/generated/types'; + +import type { TFolderType } from '../../types/folder-type'; +import { FolderType } from '../../types/folder-type'; + +export interface UpdateFolderOptions { + userId: number; + teamId?: number; + folderId: string; + name: string; + visibility: DocumentVisibility; + type?: TFolderType; +} + +export const updateFolder = async ({ + userId, + teamId, + folderId, + name, + visibility, + type, +}: UpdateFolderOptions) => { + const folder = await prisma.folder.findFirst({ + where: { + id: folderId, + userId, + teamId, + type, + }, + }); + + if (!folder) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Folder not found', + }); + } + + const isTemplateFolder = folder.type === FolderType.TEMPLATE; + const effectiveVisibility = + isTemplateFolder && teamId !== null ? DocumentVisibility.EVERYONE : visibility; + + return await prisma.folder.update({ + where: { + id: folderId, + }, + data: { + name, + visibility: effectiveVisibility, + }, + }); +}; diff --git a/packages/lib/server-only/template/create-template.ts b/packages/lib/server-only/template/create-template.ts index 8e884cce4..d2725eb41 100644 --- a/packages/lib/server-only/template/create-template.ts +++ b/packages/lib/server-only/template/create-template.ts @@ -20,6 +20,7 @@ export const createTemplate = async ({ userId, teamId, templateDocumentDataId, + folderId, }: CreateTemplateOptions) => { let team = null; @@ -43,12 +44,38 @@ export const createTemplate = async ({ } } + const folder = await prisma.folder.findFirstOrThrow({ + where: { + id: folderId, + ...(teamId + ? { + team: { + id: teamId, + members: { + some: { + userId, + }, + }, + }, + } + : { + userId, + teamId: null, + }), + }, + }); + + if (!team) { + throw new AppError(AppErrorCode.NOT_FOUND); + } + return await prisma.template.create({ data: { title, userId, templateDocumentDataId, teamId, + folderId: folder.id, templateMeta: { create: { language: team?.teamGlobalSettings?.documentLanguage, diff --git a/packages/lib/server-only/template/find-templates.ts b/packages/lib/server-only/template/find-templates.ts index 94ba44fa2..206178eb4 100644 --- a/packages/lib/server-only/template/find-templates.ts +++ b/packages/lib/server-only/template/find-templates.ts @@ -12,6 +12,7 @@ export type FindTemplatesOptions = { type?: Template['type']; page?: number; perPage?: number; + folderId?: string; }; export const findTemplates = async ({ @@ -20,6 +21,7 @@ export const findTemplates = async ({ type, page = 1, perPage = 10, + folderId, }: FindTemplatesOptions) => { const whereFilter: Prisma.TemplateWhereInput[] = []; @@ -67,6 +69,12 @@ export const findTemplates = async ({ ); } + if (folderId) { + whereFilter.push({ folderId }); + } else { + whereFilter.push({ folderId: null }); + } + const [data, count] = await Promise.all([ prisma.template.findMany({ where: { diff --git a/packages/lib/server-only/template/get-template-by-id.ts b/packages/lib/server-only/template/get-template-by-id.ts index e978d75bb..3652d1643 100644 --- a/packages/lib/server-only/template/get-template-by-id.ts +++ b/packages/lib/server-only/template/get-template-by-id.ts @@ -6,9 +6,15 @@ export type GetTemplateByIdOptions = { id: number; userId: number; teamId?: number; + folderId?: string | null; }; -export const getTemplateById = async ({ id, userId, teamId }: GetTemplateByIdOptions) => { +export const getTemplateById = async ({ + id, + userId, + teamId, + folderId = null, +}: GetTemplateByIdOptions) => { const template = await prisma.template.findFirst({ where: { id, @@ -27,6 +33,7 @@ export const getTemplateById = async ({ id, userId, teamId }: GetTemplateByIdOpt userId, teamId: null, }), + ...(folderId ? { folderId } : {}), }, include: { directLink: true, @@ -41,6 +48,7 @@ export const getTemplateById = async ({ id, userId, teamId }: GetTemplateByIdOpt email: true, }, }, + folder: true, }, }); diff --git a/packages/lib/types/document.ts b/packages/lib/types/document.ts index 0835f8ce7..2b2fe8284 100644 --- a/packages/lib/types/document.ts +++ b/packages/lib/types/document.ts @@ -3,6 +3,7 @@ import type { z } from 'zod'; import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema'; import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema'; import { DocumentSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentSchema'; +import { FolderSchema } from '@documenso/prisma/generated/zod/modelSchema/FolderSchema'; import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema'; import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema'; @@ -31,6 +32,7 @@ export const ZDocumentSchema = DocumentSchema.pick({ deletedAt: true, teamId: true, templateId: true, + folderId: true, }).extend({ // Todo: Maybe we want to alter this a bit since this returns a lot of data. documentData: DocumentDataSchema.pick({ @@ -57,6 +59,18 @@ export const ZDocumentSchema = DocumentSchema.pick({ language: true, emailSettings: true, }).nullable(), + folder: FolderSchema.pick({ + id: true, + name: true, + type: true, + visibility: true, + userId: true, + teamId: true, + pinned: true, + parentId: true, + createdAt: true, + updatedAt: true, + }).nullable(), recipients: ZRecipientLiteSchema.array(), fields: ZFieldSchema.array(), }); @@ -83,6 +97,7 @@ export const ZDocumentLiteSchema = DocumentSchema.pick({ deletedAt: true, teamId: true, templateId: true, + folderId: true, useLegacyFieldInsertion: true, }); @@ -108,6 +123,7 @@ export const ZDocumentManySchema = DocumentSchema.pick({ deletedAt: true, teamId: true, templateId: true, + folderId: true, useLegacyFieldInsertion: true, }).extend({ user: UserSchema.pick({ diff --git a/packages/lib/types/folder-type.ts b/packages/lib/types/folder-type.ts new file mode 100644 index 000000000..fd169c4b0 --- /dev/null +++ b/packages/lib/types/folder-type.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +export const FolderType = { + DOCUMENT: 'DOCUMENT', + TEMPLATE: 'TEMPLATE', +} as const; + +export const ZFolderTypeSchema = z.enum([FolderType.DOCUMENT, FolderType.TEMPLATE]); +export type TFolderType = z.infer; diff --git a/packages/lib/types/template.ts b/packages/lib/types/template.ts index 2f36336c4..f5f5dd6f0 100644 --- a/packages/lib/types/template.ts +++ b/packages/lib/types/template.ts @@ -1,6 +1,7 @@ import type { z } from 'zod'; import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema'; +import { FolderSchema } from '@documenso/prisma/generated/zod/modelSchema/FolderSchema'; import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema'; import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateDirectLinkSchema'; import { TemplateMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateMetaSchema'; @@ -29,6 +30,7 @@ export const ZTemplateSchema = TemplateSchema.pick({ updatedAt: true, publicTitle: true, publicDescription: true, + folderId: true, }).extend({ // Todo: Maybe we want to alter this a bit since this returns a lot of data. templateDocumentData: DocumentDataSchema.pick({ @@ -62,6 +64,18 @@ export const ZTemplateSchema = TemplateSchema.pick({ }), recipients: ZRecipientLiteSchema.array(), fields: ZFieldSchema.array(), + folder: FolderSchema.pick({ + id: true, + name: true, + type: true, + visibility: true, + userId: true, + teamId: true, + pinned: true, + parentId: true, + createdAt: true, + updatedAt: true, + }).nullable(), }); export type TTemplate = z.infer; @@ -83,6 +97,7 @@ export const ZTemplateLiteSchema = TemplateSchema.pick({ updatedAt: true, publicTitle: true, publicDescription: true, + folderId: true, useLegacyFieldInsertion: true, }); @@ -103,6 +118,7 @@ export const ZTemplateManySchema = TemplateSchema.pick({ updatedAt: true, publicTitle: true, publicDescription: true, + folderId: true, useLegacyFieldInsertion: true, }).extend({ team: TeamSchema.pick({ diff --git a/packages/lib/utils/format-folder-count.ts b/packages/lib/utils/format-folder-count.ts new file mode 100644 index 000000000..ebd822554 --- /dev/null +++ b/packages/lib/utils/format-folder-count.ts @@ -0,0 +1,5 @@ +export function formatFolderCount(count: number, singular: string, plural?: string): string { + const itemLabel = count === 1 ? singular : plural || `${singular}s`; + + return `${count} ${itemLabel}`; +} diff --git a/packages/prisma/migrations/20250318072444_add_document_folders/migration.sql b/packages/prisma/migrations/20250318072444_add_document_folders/migration.sql new file mode 100644 index 000000000..a29ece39d --- /dev/null +++ b/packages/prisma/migrations/20250318072444_add_document_folders/migration.sql @@ -0,0 +1,39 @@ +-- AlterTable +ALTER TABLE "Document" ADD COLUMN "folderId" INTEGER; + +-- CreateTable +CREATE TABLE "Folder" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "userId" INTEGER NOT NULL, + "teamId" INTEGER, + "parentId" INTEGER, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Folder_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "Folder_userId_idx" ON "Folder"("userId"); + +-- CreateIndex +CREATE INDEX "Folder_teamId_idx" ON "Folder"("teamId"); + +-- CreateIndex +CREATE INDEX "Folder_parentId_idx" ON "Folder"("parentId"); + +-- CreateIndex +CREATE INDEX "Document_folderId_idx" ON "Document"("folderId"); + +-- AddForeignKey +ALTER TABLE "Folder" ADD CONSTRAINT "Folder_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Folder" ADD CONSTRAINT "Folder_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Folder" ADD CONSTRAINT "Folder_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Folder"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Document" ADD CONSTRAINT "Document_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20250324135010_model_folder_update_id_type_and_add_pinned_property/migration.sql b/packages/prisma/migrations/20250324135010_model_folder_update_id_type_and_add_pinned_property/migration.sql new file mode 100644 index 000000000..a7a232d4a --- /dev/null +++ b/packages/prisma/migrations/20250324135010_model_folder_update_id_type_and_add_pinned_property/migration.sql @@ -0,0 +1,29 @@ +/* + Warnings: + + - The primary key for the `Folder` table will be changed. If it partially fails, the table could be left without primary key constraint. + +*/ +-- DropForeignKey +ALTER TABLE "Document" DROP CONSTRAINT "Document_folderId_fkey"; + +-- DropForeignKey +ALTER TABLE "Folder" DROP CONSTRAINT "Folder_parentId_fkey"; + +-- AlterTable +ALTER TABLE "Document" ALTER COLUMN "folderId" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "Folder" DROP CONSTRAINT "Folder_pkey", +ADD COLUMN "pinned" BOOLEAN NOT NULL DEFAULT false, +ALTER COLUMN "id" DROP DEFAULT, +ALTER COLUMN "id" SET DATA TYPE TEXT, +ALTER COLUMN "parentId" SET DATA TYPE TEXT, +ADD CONSTRAINT "Folder_pkey" PRIMARY KEY ("id"); +DROP SEQUENCE "Folder_id_seq"; + +-- AddForeignKey +ALTER TABLE "Folder" ADD CONSTRAINT "Folder_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Folder"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Document" ADD CONSTRAINT "Document_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20250328135807_add_folder_visibility/migration.sql b/packages/prisma/migrations/20250328135807_add_folder_visibility/migration.sql new file mode 100644 index 000000000..0b21867ee --- /dev/null +++ b/packages/prisma/migrations/20250328135807_add_folder_visibility/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Folder" ADD COLUMN "visibility" "DocumentVisibility" NOT NULL DEFAULT 'EVERYONE'; diff --git a/packages/prisma/migrations/20250404111643_add_folder_type_and_template_folders/migration.sql b/packages/prisma/migrations/20250404111643_add_folder_type_and_template_folders/migration.sql new file mode 100644 index 000000000..8d9bb1471 --- /dev/null +++ b/packages/prisma/migrations/20250404111643_add_folder_type_and_template_folders/migration.sql @@ -0,0 +1,20 @@ +/* + Warnings: + + - Added the required column `type` to the `Folder` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "FolderType" AS ENUM ('DOCUMENT', 'TEMPLATE'); + +-- AlterTable +ALTER TABLE "Folder" ADD COLUMN "type" "FolderType" NOT NULL; + +-- AlterTable +ALTER TABLE "Template" ADD COLUMN "folderId" TEXT; + +-- CreateIndex +CREATE INDEX "Folder_type_idx" ON "Folder"("type"); + +-- AddForeignKey +ALTER TABLE "Template" ADD CONSTRAINT "Template_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20250407124054_add_delete_oncascade_folder/migration.sql b/packages/prisma/migrations/20250407124054_add_delete_oncascade_folder/migration.sql new file mode 100644 index 000000000..5ba34c882 --- /dev/null +++ b/packages/prisma/migrations/20250407124054_add_delete_oncascade_folder/migration.sql @@ -0,0 +1,11 @@ +-- DropForeignKey +ALTER TABLE "Document" DROP CONSTRAINT "Document_folderId_fkey"; + +-- DropForeignKey +ALTER TABLE "Template" DROP CONSTRAINT "Template_folderId_fkey"; + +-- AddForeignKey +ALTER TABLE "Document" ADD CONSTRAINT "Document_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Template" ADD CONSTRAINT "Template_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index c5b0069b0..56558ad6c 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -56,6 +56,7 @@ model User { accounts Account[] sessions Session[] documents Document[] + folders Folder[] subscriptions Subscription[] passwordResetTokens PasswordResetToken[] ownedTeams Team[] @@ -312,6 +313,35 @@ enum DocumentVisibility { ADMIN } +enum FolderType { + DOCUMENT + TEMPLATE +} + +model Folder { + id String @id @default(cuid()) + name String + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + teamId Int? + team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade) + pinned Boolean @default(false) + parentId String? + parent Folder? @relation("FolderToFolder", fields: [parentId], references: [id], onDelete: Cascade) + documents Document[] + templates Template[] + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + subfolders Folder[] @relation("FolderToFolder") + visibility DocumentVisibility @default(EVERYONE) + type FolderType + + @@index([userId]) + @@index([teamId]) + @@index([parentId]) + @@index([type]) +} + /// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';", "import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';"]) model Document { id Int @id @default(autoincrement()) @@ -344,10 +374,13 @@ model Document { template Template? @relation(fields: [templateId], references: [id], onDelete: SetNull) auditLogs DocumentAuditLog[] + folder Folder? @relation(fields: [folderId], references: [id], onDelete: Cascade) + folderId String? @@unique([documentDataId]) @@index([userId]) @@index([status]) + @@index([folderId]) } model DocumentAuditLog { @@ -593,6 +626,7 @@ model Team { documents Document[] templates Template[] + folders Folder[] apiTokens ApiToken[] webhooks Webhook[] } @@ -721,6 +755,8 @@ model Template { fields Field[] directLink TemplateDirectLink? documents Document[] + folder Folder? @relation(fields: [folderId], references: [id], onDelete: Cascade) + folderId String? @@unique([templateDocumentDataId]) } diff --git a/packages/prisma/seed/folders.ts b/packages/prisma/seed/folders.ts new file mode 100644 index 000000000..d388eaca9 --- /dev/null +++ b/packages/prisma/seed/folders.ts @@ -0,0 +1,33 @@ +import type { User } from '@prisma/client'; +import { DocumentStatus, FolderType } from '@prisma/client'; + +import { prisma } from '..'; +import type { Prisma } from '../client'; +import { seedDocuments } from './documents'; + +type CreateFolderOptions = { + type?: string; + createFolderOptions?: Partial; +}; + +export const seedBlankFolder = async (user: User, options: CreateFolderOptions = {}) => { + return await prisma.folder.create({ + data: { + name: 'My folder', + userId: user.id, + type: FolderType.DOCUMENT, + ...options.createFolderOptions, + }, + }); +}; + +export const seedFolderWithDocuments = async (user: User, options: CreateFolderOptions = {}) => { + const folder = await seedBlankFolder(user, options); + await seedDocuments([ + { + sender: user, + recipients: [user], + type: DocumentStatus.DRAFT, + }, + ]); +}; diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index 53b5bbd30..2a14ed30e 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -107,8 +107,17 @@ export const documentRouter = router({ .query(async ({ input, ctx }) => { const { user, teamId } = ctx; - const { query, templateId, page, perPage, orderByDirection, orderByColumn, source, status } = - input; + const { + query, + templateId, + page, + perPage, + orderByDirection, + orderByColumn, + source, + status, + folderId, + } = input; const documents = await findDocuments({ userId: user.id, @@ -119,6 +128,7 @@ export const documentRouter = router({ status, page, perPage, + folderId, orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined, }); @@ -147,12 +157,14 @@ export const documentRouter = router({ status, period, senderIds, + folderId, } = input; const getStatOptions: GetStatsInput = { user, period, search: query, + folderId, }; if (teamId) { @@ -181,6 +193,7 @@ export const documentRouter = router({ status, period, senderIds, + folderId, orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined, @@ -212,12 +225,13 @@ export const documentRouter = router({ .output(ZGetDocumentWithDetailsByIdResponseSchema) .query(async ({ input, ctx }) => { const { teamId, user } = ctx; - const { documentId } = input; + const { documentId, folderId } = input; return await getDocumentWithDetailsById({ userId: user.id, teamId, documentId, + folderId, }); }), @@ -290,6 +304,7 @@ export const documentRouter = router({ return { document: createdDocument, + folder: createdDocument.folder, uploadUrl: url, }; }), @@ -311,7 +326,7 @@ export const documentRouter = router({ .input(ZCreateDocumentRequestSchema) .mutation(async ({ input, ctx }) => { const { teamId } = ctx; - const { title, documentDataId, timezone } = input; + const { title, documentDataId, timezone, folderId } = input; const { remaining } = await getServerLimits({ email: ctx.user.email, teamId }); @@ -330,6 +345,7 @@ export const documentRouter = router({ normalizePdf: true, timezone, requestMetadata: ctx.metadata, + folderId, }); }), diff --git a/packages/trpc/server/document-router/schema.ts b/packages/trpc/server/document-router/schema.ts index 2b5e06a3d..ac977de28 100644 --- a/packages/trpc/server/document-router/schema.ts +++ b/packages/trpc/server/document-router/schema.ts @@ -130,6 +130,7 @@ export const ZFindDocumentsRequestSchema = ZFindSearchParamsSchema.extend({ .nativeEnum(DocumentStatus) .describe('Filter documents by the current status') .optional(), + folderId: z.string().describe('Filter documents by folder ID').optional(), orderByColumn: z.enum(['createdAt']).optional(), orderByDirection: z.enum(['asc', 'desc']).describe('').default('desc'), }); @@ -144,6 +145,7 @@ export const ZFindDocumentsInternalRequestSchema = ZFindDocumentsRequestSchema.e period: z.enum(['7d', '14d', '30d']).optional(), senderIds: z.array(z.number()).optional(), status: z.nativeEnum(ExtendedDocumentStatus).optional(), + folderId: z.string().optional(), }); export const ZFindDocumentsInternalResponseSchema = ZFindResultResponse.extend({ @@ -188,6 +190,7 @@ export type TGetDocumentByTokenQuerySchema = z.infer { + const { teamId, user } = ctx; + const { parentId, type } = input; + + const folders = await findFolders({ + userId: user.id, + teamId, + parentId, + type, + }); + + const breadcrumbs = parentId + ? await getFolderBreadcrumbs({ + userId: user.id, + teamId, + folderId: parentId, + type, + }) + : []; + + return { + folders, + breadcrumbs, + type, + }; + }), + + /** + * @private + */ + findFolders: authenticatedProcedure + .input(ZFindFoldersRequestSchema) + .output(ZFindFoldersResponseSchema) + .query(async ({ input, ctx }) => { + const { teamId, user } = ctx; + const { parentId, type } = input; + + const folders = await findFolders({ + userId: user.id, + teamId, + parentId, + type, + }); + + const breadcrumbs = parentId + ? await getFolderBreadcrumbs({ + userId: user.id, + teamId, + folderId: parentId, + type, + }) + : []; + + return { + data: folders, + breadcrumbs, + type, + }; + }), + + /** + * @private + */ + createFolder: authenticatedProcedure + .input(ZCreateFolderSchema) + .mutation(async ({ input, ctx }) => { + const { teamId, user } = ctx; + const { name, parentId, type } = input; + + if (parentId) { + try { + await getFolderById({ + userId: user.id, + teamId, + folderId: parentId, + type, + }); + } catch (error) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Parent folder not found', + }); + } + } + + const result = await createFolder({ + userId: user.id, + teamId, + name, + parentId, + type, + }); + + return { + ...result, + type, + }; + }), + + /** + * @private + */ + updateFolder: authenticatedProcedure + .input(ZUpdateFolderSchema) + .mutation(async ({ input, ctx }) => { + const { teamId, user } = ctx; + const { id, name, visibility } = input; + + const currentFolder = await getFolderById({ + userId: user.id, + teamId, + folderId: id, + }); + + const result = await updateFolder({ + userId: user.id, + teamId, + folderId: id, + name, + visibility, + type: currentFolder.type, + }); + + return { + ...result, + type: currentFolder.type, + }; + }), + + /** + * @private + */ + deleteFolder: authenticatedProcedure + .input(ZDeleteFolderSchema) + .output(ZSuccessResponseSchema) + .mutation(async ({ input, ctx }) => { + const { teamId, user } = ctx; + const { id } = input; + + await deleteFolder({ + userId: user.id, + teamId, + folderId: id, + }); + + return ZGenericSuccessResponse; + }), + + /** + * @private + */ + moveFolder: authenticatedProcedure.input(ZMoveFolderSchema).mutation(async ({ input, ctx }) => { + const { teamId, user } = ctx; + const { id, parentId } = input; + + const currentFolder = await getFolderById({ + userId: user.id, + teamId, + folderId: id, + }); + + if (parentId !== null) { + try { + await getFolderById({ + userId: user.id, + teamId, + folderId: parentId, + type: currentFolder.type, + }); + } catch (error) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Parent folder not found', + }); + } + } + + const result = await moveFolder({ + userId: user.id, + teamId, + folderId: id, + parentId, + requestMetadata: ctx.metadata, + }); + + return { + ...result, + type: currentFolder.type, + }; + }), + + /** + * @private + */ + moveDocumentToFolder: authenticatedProcedure + .input(ZMoveDocumentToFolderSchema) + .mutation(async ({ input, ctx }) => { + const { teamId, user } = ctx; + const { documentId, folderId } = input; + + if (folderId !== null) { + try { + await getFolderById({ + userId: user.id, + teamId, + folderId, + type: FolderType.DOCUMENT, + }); + } catch (error) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Folder not found', + }); + } + } + + const result = await moveDocumentToFolder({ + userId: user.id, + teamId, + documentId, + folderId, + requestMetadata: ctx.metadata, + }); + + return { + ...result, + type: FolderType.DOCUMENT, + }; + }), + + /** + * @private + */ + moveTemplateToFolder: authenticatedProcedure + .input(ZMoveTemplateToFolderSchema) + .mutation(async ({ input, ctx }) => { + const { teamId, user } = ctx; + const { templateId, folderId } = input; + + if (folderId !== null) { + try { + await getFolderById({ + userId: user.id, + teamId, + folderId, + type: FolderType.TEMPLATE, + }); + } catch (error) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Folder not found', + }); + } + } + + const result = await moveTemplateToFolder({ + userId: user.id, + teamId, + templateId, + folderId, + }); + + return { + ...result, + type: FolderType.TEMPLATE, + }; + }), + + /** + * @private + */ + pinFolder: authenticatedProcedure.input(ZPinFolderSchema).mutation(async ({ ctx, input }) => { + const currentFolder = await getFolderById({ + userId: ctx.user.id, + teamId: ctx.teamId, + folderId: input.folderId, + }); + + const result = await pinFolder({ + userId: ctx.user.id, + teamId: ctx.teamId, + folderId: input.folderId, + type: currentFolder.type, + }); + + return { + ...result, + type: currentFolder.type, + }; + }), + + /** + * @private + */ + unpinFolder: authenticatedProcedure.input(ZUnpinFolderSchema).mutation(async ({ ctx, input }) => { + const currentFolder = await getFolderById({ + userId: ctx.user.id, + teamId: ctx.teamId, + folderId: input.folderId, + }); + + const result = await unpinFolder({ + userId: ctx.user.id, + teamId: ctx.teamId, + folderId: input.folderId, + type: currentFolder.type, + }); + + return { + ...result, + type: currentFolder.type, + }; + }), +}); diff --git a/packages/trpc/server/folder-router/schema.ts b/packages/trpc/server/folder-router/schema.ts new file mode 100644 index 000000000..72d8e17d9 --- /dev/null +++ b/packages/trpc/server/folder-router/schema.ts @@ -0,0 +1,132 @@ +import { z } from 'zod'; + +import { ZFolderTypeSchema } from '@documenso/lib/types/folder-type'; +import { DocumentVisibility } from '@documenso/prisma/generated/types'; + +/** + * Required for empty responses since we currently can't 201 requests for our openapi setup. + * + * Without this it will throw an error in Speakeasy SDK when it tries to parse an empty response. + */ +export const ZSuccessResponseSchema = z.object({ + success: z.boolean(), + type: ZFolderTypeSchema.optional(), +}); + +export const ZGenericSuccessResponse = { + success: true, +} satisfies z.infer; + +export const ZFolderSchema = z.object({ + id: z.string(), + name: z.string(), + userId: z.number(), + teamId: z.number().nullable(), + parentId: z.string().nullable(), + pinned: z.boolean(), + createdAt: z.date(), + updatedAt: z.date(), + visibility: z.nativeEnum(DocumentVisibility), + type: ZFolderTypeSchema, +}); + +export type TFolder = z.infer; + +const ZFolderCountSchema = z.object({ + documents: z.number(), + templates: z.number(), + subfolders: z.number(), +}); + +const ZSubfolderSchema = ZFolderSchema.extend({ + subfolders: z.array(z.any()), + _count: ZFolderCountSchema, +}); + +export const ZFolderWithSubfoldersSchema = ZFolderSchema.extend({ + subfolders: z.array(ZSubfolderSchema), + _count: ZFolderCountSchema, +}); + +export type TFolderWithSubfolders = z.infer; + +export const ZCreateFolderSchema = z.object({ + name: z.string(), + parentId: z.string().optional(), + type: ZFolderTypeSchema.optional(), +}); + +export const ZUpdateFolderSchema = z.object({ + id: z.string(), + name: z.string(), + visibility: z.nativeEnum(DocumentVisibility), + type: ZFolderTypeSchema.optional(), +}); + +export type TUpdateFolderSchema = z.infer; + +export const ZDeleteFolderSchema = z.object({ + id: z.string(), + type: ZFolderTypeSchema.optional(), +}); + +export const ZMoveFolderSchema = z.object({ + id: z.string(), + parentId: z.string().nullable(), + type: ZFolderTypeSchema.optional(), +}); + +export const ZMoveDocumentToFolderSchema = z.object({ + documentId: z.number(), + folderId: z.string().nullable().optional(), + type: z.enum(['DOCUMENT']).optional(), +}); + +export const ZMoveTemplateToFolderSchema = z.object({ + templateId: z.number(), + folderId: z.string().nullable().optional(), + type: z.enum(['TEMPLATE']).optional(), +}); + +export const ZPinFolderSchema = z.object({ + folderId: z.string(), + type: ZFolderTypeSchema.optional(), +}); + +export const ZUnpinFolderSchema = z.object({ + folderId: z.string(), + type: ZFolderTypeSchema.optional(), +}); + +export const ZGetFoldersSchema = z.object({ + parentId: z.string().nullable().optional(), + type: ZFolderTypeSchema.optional(), +}); + +export const ZGetFoldersResponseSchema = z.object({ + folders: z.array(ZFolderWithSubfoldersSchema), + breadcrumbs: z.array(ZFolderSchema), + type: ZFolderTypeSchema.optional(), +}); + +export type TGetFoldersResponse = z.infer; + +export const ZFindSearchParamsSchema = z.object({ + query: z.string().optional(), + page: z.number().optional(), + perPage: z.number().optional(), + type: ZFolderTypeSchema.optional(), +}); + +export const ZFindFoldersRequestSchema = ZFindSearchParamsSchema.extend({ + parentId: z.string().nullable().optional(), + type: ZFolderTypeSchema.optional(), +}); + +export const ZFindFoldersResponseSchema = z.object({ + data: z.array(ZFolderWithSubfoldersSchema), + breadcrumbs: z.array(ZFolderSchema), + type: ZFolderTypeSchema.optional(), +}); + +export type TFindFoldersResponse = z.infer; diff --git a/packages/trpc/server/router.ts b/packages/trpc/server/router.ts index 8096bd121..efd133547 100644 --- a/packages/trpc/server/router.ts +++ b/packages/trpc/server/router.ts @@ -4,6 +4,7 @@ import { authRouter } from './auth-router/router'; import { documentRouter } from './document-router/router'; import { embeddingPresignRouter } from './embedding-router/_router'; import { fieldRouter } from './field-router/router'; +import { folderRouter } from './folder-router/router'; import { profileRouter } from './profile-router/router'; import { recipientRouter } from './recipient-router/router'; import { shareLinkRouter } from './share-link-router/router'; @@ -17,6 +18,7 @@ export const appRouter = router({ profile: profileRouter, document: documentRouter, field: fieldRouter, + folder: folderRouter, recipient: recipientRouter, admin: adminRouter, shareLink: shareLinkRouter, diff --git a/packages/trpc/server/template-router/router.ts b/packages/trpc/server/template-router/router.ts index 3b3f13218..b11fae91a 100644 --- a/packages/trpc/server/template-router/router.ts +++ b/packages/trpc/server/template-router/router.ts @@ -121,13 +121,14 @@ export const templateRouter = router({ .output(ZCreateTemplateResponseSchema) .mutation(async ({ input, ctx }) => { const { teamId } = ctx; - const { title, templateDocumentDataId } = input; + const { title, templateDocumentDataId, folderId } = input; return await createTemplate({ userId: ctx.user.id, teamId, title, templateDocumentDataId, + folderId, }); }), diff --git a/packages/trpc/server/template-router/schema.ts b/packages/trpc/server/template-router/schema.ts index 2d8481395..fc193abd7 100644 --- a/packages/trpc/server/template-router/schema.ts +++ b/packages/trpc/server/template-router/schema.ts @@ -33,6 +33,7 @@ import { ZSignFieldWithTokenMutationSchema } from '../field-router/schema'; export const ZCreateTemplateMutationSchema = z.object({ title: z.string().min(1).trim(), templateDocumentDataId: z.string().min(1), + folderId: z.string().optional(), }); export const ZCreateDocumentFromDirectTemplateRequestSchema = z.object({ @@ -179,6 +180,7 @@ export const ZUpdateTemplateResponseSchema = ZTemplateLiteSchema; export const ZFindTemplatesRequestSchema = ZFindSearchParamsSchema.extend({ type: z.nativeEnum(TemplateType).describe('Filter templates by type.').optional(), + folderId: z.string().describe('The ID of the folder to filter templates by.').optional(), }); export const ZFindTemplatesResponseSchema = ZFindResultResponse.extend({ diff --git a/packages/ui/primitives/document-upload.tsx b/packages/ui/primitives/document-upload.tsx new file mode 100644 index 000000000..57fb24a0a --- /dev/null +++ b/packages/ui/primitives/document-upload.tsx @@ -0,0 +1,88 @@ +import type { MessageDescriptor } from '@lingui/core'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { Upload } from 'lucide-react'; +import { useDropzone } from 'react-dropzone'; +import { Link } from 'react-router'; + +import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; +import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions'; + +import { Button } from './button'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip'; + +export type DocumentDropzoneProps = { + className?: string; + disabled?: boolean; + disabledMessage?: MessageDescriptor; + onDrop?: (_file: File) => void | Promise; + onDropRejected?: () => void | Promise; + type?: 'document' | 'template'; + [key: string]: unknown; +}; + +export const DocumentDropzone = ({ + className, + onDrop, + onDropRejected, + disabled, + disabledMessage = msg`You cannot upload documents at this time.`, + type = 'document', + ...props +}: DocumentDropzoneProps) => { + const { _ } = useLingui(); + + const { getRootProps, getInputProps } = useDropzone({ + accept: { + 'application/pdf': ['.pdf'], + }, + multiple: false, + disabled, + onDrop: ([acceptedFile]) => { + if (acceptedFile && onDrop) { + void onDrop(acceptedFile); + } + }, + onDropRejected: () => { + if (onDropRejected) { + void onDropRejected(); + } + }, + maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT), + }); + + const heading = { + document: msg`Upload Document`, + template: msg`Upload Template Document`, + }; + + if (disabled && IS_BILLING_ENABLED()) { + return ( + + + + + + +

{_(disabledMessage)}

+
+
+
+ ); + } + + return ( + + ); +}; From e108da546dba538ca95fd144705d60aee5707d58 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Fri, 2 May 2025 10:50:13 +1000 Subject: [PATCH 05/12] fix: incorrect data for postMessage --- .../embed+/v1+/authoring+/document.edit.$id.tsx | 16 ++++++++-------- .../embed+/v1+/authoring+/template.edit.$id.tsx | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx b/apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx index 41b97e654..9bb820ec3 100644 --- a/apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +++ b/apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx @@ -1,5 +1,6 @@ import { useLayoutEffect, useMemo, useState } from 'react'; +import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { DocumentDistributionMethod, DocumentSigningOrder, SigningStatus } from '@prisma/client'; import { redirect, useLoaderData } from 'react-router'; @@ -187,8 +188,8 @@ export default function EmbeddingAuthoringDocumentEditPage() { if (!configuration) { toast({ variant: 'destructive', - title: _('Error'), - description: _('Please configure the document first'), + title: _(msg`Error`), + description: _(msg`Please configure the document first`), }); return; @@ -240,8 +241,8 @@ export default function EmbeddingAuthoringDocumentEditPage() { }); toast({ - title: _('Success'), - description: _('Document updated successfully'), + title: _(msg`Success`), + description: _(msg`Document updated successfully`), }); // Send a message to the parent window with the document details @@ -249,8 +250,7 @@ export default function EmbeddingAuthoringDocumentEditPage() { window.parent.postMessage( { type: 'document-updated', - // documentId: updateResult.documentId, - documentId: 1, + documentId: updateResult.documentId, externalId: documentExternalId, }, '*', @@ -261,8 +261,8 @@ export default function EmbeddingAuthoringDocumentEditPage() { toast({ variant: 'destructive', - title: _('Error'), - description: _('Failed to update document'), + title: _(msg`Error`), + description: _(msg`Failed to update document`), }); } }; diff --git a/apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx b/apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx index 7c5cef644..dbf88ad1c 100644 --- a/apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +++ b/apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx @@ -1,5 +1,6 @@ import { useLayoutEffect, useMemo, useState } from 'react'; +import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { DocumentDistributionMethod, DocumentSigningOrder, SigningStatus } from '@prisma/client'; import { redirect, useLoaderData } from 'react-router'; @@ -187,8 +188,8 @@ export default function EmbeddingAuthoringTemplateEditPage() { if (!configuration) { toast({ variant: 'destructive', - title: _('Error'), - description: _('Please configure the document first'), + title: _(msg`Error`), + description: _(msg`Please configure the document first`), }); return; @@ -240,8 +241,8 @@ export default function EmbeddingAuthoringTemplateEditPage() { }); toast({ - title: _('Success'), - description: _('Document updated successfully'), + title: _(msg`Success`), + description: _(msg`Template updated successfully`), }); // Send a message to the parent window with the template details @@ -249,8 +250,7 @@ export default function EmbeddingAuthoringTemplateEditPage() { window.parent.postMessage( { type: 'template-updated', - // templateId: updateResult.templateId, - templateId: 1, + templateId: updateResult.templateId, externalId: templateExternalId, }, '*', @@ -261,8 +261,8 @@ export default function EmbeddingAuthoringTemplateEditPage() { toast({ variant: 'destructive', - title: _('Error'), - description: _('Failed to update template'), + title: _(msg`Error`), + description: _(msg`Failed to update template`), }); } }; From 8c9dd5e372c40bfb8a36fdff9a6addb18df6e6e6 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Fri, 2 May 2025 12:03:08 +1000 Subject: [PATCH 06/12] v1.10.0 --- apps/remix/package.json | 2 +- package-lock.json | 6 +++--- package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/remix/package.json b/apps/remix/package.json index bf1e33c1d..b2088f9a2 100644 --- a/apps/remix/package.json +++ b/apps/remix/package.json @@ -100,5 +100,5 @@ "vite-plugin-babel-macros": "^1.0.6", "vite-tsconfig-paths": "^5.1.4" }, - "version": "1.10.0-rc.5" + "version": "1.10.0" } diff --git a/package-lock.json b/package-lock.json index 36e11e4c3..6f91edba5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@documenso/root", - "version": "1.10.0-rc.5", + "version": "1.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@documenso/root", - "version": "1.10.0-rc.5", + "version": "1.10.0", "workspaces": [ "apps/*", "packages/*" @@ -95,7 +95,7 @@ }, "apps/remix": { "name": "@documenso/remix", - "version": "1.10.0-rc.5", + "version": "1.10.0", "dependencies": { "@documenso/api": "*", "@documenso/assets": "*", diff --git a/package.json b/package.json index e899c6659..3d6e7f2be 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "1.10.0-rc.5", + "version": "1.10.0", "scripts": { "build": "turbo run build", "dev": "turbo run dev --filter=@documenso/remix", From 0931c472a77b1a15e06c3e9c723a42f35aba928f Mon Sep 17 00:00:00 2001 From: Mythie Date: Sat, 3 May 2025 07:38:48 +1000 Subject: [PATCH 07/12] fix: resolve issue with uploading templates --- .../server-only/template/create-template.ts | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/packages/lib/server-only/template/create-template.ts b/packages/lib/server-only/template/create-template.ts index d2725eb41..df4ed873f 100644 --- a/packages/lib/server-only/template/create-template.ts +++ b/packages/lib/server-only/template/create-template.ts @@ -44,26 +44,34 @@ export const createTemplate = async ({ } } - const folder = await prisma.folder.findFirstOrThrow({ - where: { - id: folderId, - ...(teamId - ? { - team: { - id: teamId, - members: { - some: { - userId, + if (folderId) { + const folder = await prisma.folder.findFirst({ + where: { + id: folderId, + ...(teamId + ? { + team: { + id: teamId, + members: { + some: { + userId, + }, }, }, - }, - } - : { - userId, - teamId: null, - }), - }, - }); + } + : { + userId, + teamId: null, + }), + }, + }); + + if (!folder) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Folder not found', + }); + } + } if (!team) { throw new AppError(AppErrorCode.NOT_FOUND); From ac7d24eb124bc792125a6190f83ba2240066547e Mon Sep 17 00:00:00 2001 From: Mythie Date: Sat, 3 May 2025 07:39:19 +1000 Subject: [PATCH 08/12] v1.10.1 --- apps/remix/package.json | 2 +- package-lock.json | 6 +++--- package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/remix/package.json b/apps/remix/package.json index b2088f9a2..e30650f41 100644 --- a/apps/remix/package.json +++ b/apps/remix/package.json @@ -100,5 +100,5 @@ "vite-plugin-babel-macros": "^1.0.6", "vite-tsconfig-paths": "^5.1.4" }, - "version": "1.10.0" + "version": "1.10.1" } diff --git a/package-lock.json b/package-lock.json index 6f91edba5..a6fa36e63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@documenso/root", - "version": "1.10.0", + "version": "1.10.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@documenso/root", - "version": "1.10.0", + "version": "1.10.1", "workspaces": [ "apps/*", "packages/*" @@ -95,7 +95,7 @@ }, "apps/remix": { "name": "@documenso/remix", - "version": "1.10.0", + "version": "1.10.1", "dependencies": { "@documenso/api": "*", "@documenso/assets": "*", diff --git a/package.json b/package.json index 3d6e7f2be..6d0db0233 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "1.10.0", + "version": "1.10.1", "scripts": { "build": "turbo run build", "dev": "turbo run dev --filter=@documenso/remix", From 516e237966bb0ee4afd4c1e98dba93d756695196 Mon Sep 17 00:00:00 2001 From: Mythie Date: Sat, 3 May 2025 08:09:44 +1000 Subject: [PATCH 09/12] fix: resolve issue with uploading templates --- packages/lib/server-only/template/create-template.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/server-only/template/create-template.ts b/packages/lib/server-only/template/create-template.ts index df4ed873f..6d4681724 100644 --- a/packages/lib/server-only/template/create-template.ts +++ b/packages/lib/server-only/template/create-template.ts @@ -83,7 +83,7 @@ export const createTemplate = async ({ userId, templateDocumentDataId, teamId, - folderId: folder.id, + folderId: folderId, templateMeta: { create: { language: team?.teamGlobalSettings?.documentLanguage, From bf1c1ff9dcae31b8e42c8fc0ffe3904b749cd766 Mon Sep 17 00:00:00 2001 From: Mythie Date: Sat, 3 May 2025 08:11:27 +1000 Subject: [PATCH 10/12] v1.10.2 --- apps/remix/package.json | 2 +- package-lock.json | 6 +- package.json | 2 +- packages/lib/translations/de/web.po | 447 +++++++++++++++++++++++++-- packages/lib/translations/en/web.po | 443 +++++++++++++++++++++++++-- packages/lib/translations/es/web.po | 450 ++++++++++++++++++++++++++-- packages/lib/translations/fr/web.po | 447 +++++++++++++++++++++++++-- packages/lib/translations/it/web.po | 450 ++++++++++++++++++++++++++-- packages/lib/translations/pl/web.po | 447 +++++++++++++++++++++++++-- 9 files changed, 2569 insertions(+), 125 deletions(-) diff --git a/apps/remix/package.json b/apps/remix/package.json index e30650f41..628fd35fc 100644 --- a/apps/remix/package.json +++ b/apps/remix/package.json @@ -100,5 +100,5 @@ "vite-plugin-babel-macros": "^1.0.6", "vite-tsconfig-paths": "^5.1.4" }, - "version": "1.10.1" + "version": "1.10.2" } diff --git a/package-lock.json b/package-lock.json index a6fa36e63..b53fda783 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@documenso/root", - "version": "1.10.1", + "version": "1.10.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@documenso/root", - "version": "1.10.1", + "version": "1.10.2", "workspaces": [ "apps/*", "packages/*" @@ -95,7 +95,7 @@ }, "apps/remix": { "name": "@documenso/remix", - "version": "1.10.1", + "version": "1.10.2", "dependencies": { "@documenso/api": "*", "@documenso/assets": "*", diff --git a/package.json b/package.json index 6d0db0233..37f9193fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "1.10.1", + "version": "1.10.2", "scripts": { "build": "turbo run build", "dev": "turbo run dev --filter=@documenso/remix", diff --git a/packages/lib/translations/de/web.po b/packages/lib/translations/de/web.po index b45b988bd..8641b8299 100644 --- a/packages/lib/translations/de/web.po +++ b/packages/lib/translations/de/web.po @@ -26,6 +26,10 @@ msgstr " Direktlink-Signierung aktivieren" msgid " The events that will trigger a webhook to be sent to your URL." msgstr " Die Ereignisse, die einen Webhook auslösen, der an Ihre URL gesendet wird." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid ".PDF documents accepted (max {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB)" +msgstr "" + #. placeholder {0}: team.name #: apps/remix/app/components/forms/team-document-preferences-form.tsx msgid "\"{0}\" has invited you to sign \"example document\"." @@ -96,11 +100,13 @@ msgid "{0, plural, one {1 matching field} other {# matching fields}}" msgstr "{0, plural, one {1 passendes Feld} other {# passende Felder}}" #. placeholder {0}: recipients.length +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.edit.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.edit.tsx msgid "{0, plural, one {1 Recipient} other {# Recipients}}" msgstr "{0, plural, one {1 Empfänger} other {# Empfänger}}" #. placeholder {0}: pendingRecipients.length +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}" msgstr "{0, plural, one {Warte auf 1 Empfänger} other {Warte auf # Empfänger}}" @@ -144,6 +150,7 @@ msgstr "{0} hat das Team {teamName} bei Documenso verlassen" #. placeholder {0}: remaining.documents #. placeholder {1}: quota.documents #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "{0} of {1} documents remaining this month." msgstr "{0} von {1} Dokumenten verbleibend in diesem Monat." @@ -161,6 +168,7 @@ msgid "{0} on behalf of \"{1}\" has invited you to {recipientActionVerb} the doc msgstr "{0} im Namen von \"{1}\" hat Sie eingeladen, das Dokument \"{2}\" {recipientActionVerb}." #. placeholder {0}: recipients.length +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "{0} Recipient(s)" msgstr "{0} Empfänger(in)" @@ -327,6 +335,10 @@ msgstr "{recipientActionVerb} Dokument" msgid "{recipientActionVerb} the document to complete the process." msgstr "{recipientActionVerb} das Dokument, um den Prozess abzuschließen." +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "{recipientCount} recipients" +msgstr "" + #: packages/email/templates/document-created-from-direct-template.tsx msgid "{recipientName} {action} a document by using one of your direct links" msgstr "{recipientName} {action} ein Dokument, indem Sie einen Ihrer direkten Links verwenden" @@ -727,6 +739,7 @@ msgstr "Hinzufügen" msgid "Add a document" msgstr "Dokument hinzufügen" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Add a URL to redirect the user to once the document is signed" @@ -800,6 +813,7 @@ msgstr "Platzhalterempfänger hinzufügen" msgid "Add Placeholders" msgstr "Platzhalter hinzufügen" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Add Signer" msgstr "Unterzeichner hinzufügen" @@ -808,6 +822,10 @@ msgstr "Unterzeichner hinzufügen" msgid "Add Signers" msgstr "Unterzeichner hinzufügen" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +msgid "Add signers and configure signing preferences" +msgstr "" + #: apps/remix/app/components/dialogs/team-email-add-dialog.tsx msgid "Add team email" msgstr "Team-E-Mail hinzufügen" @@ -853,11 +871,16 @@ msgstr "Admin-Panel" msgid "Advanced Options" msgstr "Erweiterte Optionen" +#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Advanced settings" msgstr "Erweiterte Einstellungen" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Advanced Settings" +msgstr "" + #: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx msgid "After signing a document electronically, you will be provided the opportunity to view, download, and print the document for your records. It is highly recommended that you retain a copy of all electronically signed documents for your personal records. We will also retain a copy of the signed document for our records however we may not be able to provide you with a copy of the signed document after a certain period of time." msgstr "Nach der elektronischen Unterzeichnung eines Dokuments haben Sie die Möglichkeit, das Dokument für Ihre Unterlagen anzusehen, herunterzuladen und auszudrucken. Es wird dringend empfohlen, eine Kopie aller elektronisch unterschriebenen Dokumente für Ihre persönlichen Unterlagen aufzubewahren. Wir werden ebenfalls eine Kopie des unterzeichneten Dokuments für unsere Unterlagen behalten, jedoch können wir Ihnen nach einer bestimmten Zeit möglicherweise keine Kopie des unterzeichneten Dokuments mehr zur Verfügung stellen." @@ -882,6 +905,11 @@ msgstr "Alle Dokumente wurden verarbeitet. Alle neuen Dokumente, die gesendet od msgid "All documents related to the electronic signing process will be provided to you electronically through our platform or via email. It is your responsibility to ensure that your email address is current and that you can receive and open our emails." msgstr "Alle Dokumente, die mit dem elektronischen Unterzeichnungsprozess zusammenhängen, werden Ihnen elektronisch über unsere Plattform oder per E-Mail zur Verfügung gestellt. Es liegt in Ihrer Verantwortung, sicherzustellen, dass Ihre E-Mail-Adresse aktuell ist und dass Sie unsere E-Mails empfangen und öffnen können." +#: apps/remix/app/routes/_authenticated+/templates.folders._index.tsx +#: apps/remix/app/routes/_authenticated+/documents.folders._index.tsx +msgid "All Folders" +msgstr "" + #: apps/remix/app/components/dialogs/document-delete-dialog.tsx msgid "All inserted signatures will be voided" msgstr "Alle eingefügten Unterschriften werden annulliert" @@ -910,11 +938,13 @@ msgstr "Alle Zeiten" msgid "Allow document recipients to reply directly to this email address" msgstr "Erlauben Sie den Dokumentempfängern, direkt an diese E-Mail-Adresse zu antworten" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Allow signers to dictate next signer" msgstr "Unterzeichner können nächsten Unterzeichner bestimmen" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Allowed Signature Types" @@ -998,8 +1028,11 @@ msgstr "Ein Fehler ist aufgetreten, während das direkte Links-Signieren deaktiv msgid "An error occurred while disabling the user." msgstr "Ein Fehler ist aufgetreten, während der Benutzer deaktiviert wurde." +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx +#: apps/remix/app/components/general/share-document-download-button.tsx +#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx msgid "An error occurred while downloading your document." @@ -1025,10 +1058,12 @@ msgstr "Ein Fehler ist aufgetreten, während die Teammitglieder geladen wurden. msgid "An error occurred while loading the document." msgstr "Ein Fehler ist beim Laden des Dokuments aufgetreten." +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "An error occurred while moving the document." msgstr "Ein Fehler ist aufgetreten, während das Dokument verschoben wurde." +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "An error occurred while moving the template." msgstr "Ein Fehler ist aufgetreten, während die Vorlage verschoben wurde." @@ -1103,6 +1138,7 @@ msgid "An error occurred while updating your profile." msgstr "Ein Fehler ist aufgetreten, während dein Profil aktualisiert wurde." #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "An error occurred while uploading your document." msgstr "Ein Fehler ist aufgetreten, während dein Dokument hochgeladen wurde." @@ -1140,6 +1176,21 @@ msgstr "Ein unerwarteter Fehler ist aufgetreten." msgid "An unknown error occurred" msgstr "Es ist ein unbekannter Fehler aufgetreten" +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +#: apps/remix/app/components/dialogs/folder-create-dialog.tsx +msgid "An unknown error occurred while creating the folder." +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx +msgid "An unknown error occurred while deleting the folder." +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx +#: apps/remix/app/components/dialogs/folder-move-dialog.tsx +msgid "An unknown error occurred while moving the folder." +msgstr "" + #: apps/remix/app/components/dialogs/team-transfer-dialog.tsx msgid "Any payment methods attached to this team will remain attached to this team. Please contact us if you need to update this information." msgstr "Alle Zahlungsmethoden, die mit diesem Team verbunden sind, bleiben diesem Team zugeordnet. Bitte kontaktiere uns, wenn du diese Informationen aktualisieren möchtest." @@ -1297,6 +1348,8 @@ msgstr "Warte auf E-Mail-Bestätigung" #: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx #: apps/remix/app/components/forms/signup.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx msgid "Back" msgstr "Zurück" @@ -1447,6 +1500,7 @@ msgstr "Kann vorbereiten" #: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx #: apps/remix/app/components/dialogs/webhook-create-dialog.tsx #: apps/remix/app/components/dialogs/token-delete-dialog.tsx +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx #: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx @@ -1466,6 +1520,7 @@ msgstr "Kann vorbereiten" #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx #: apps/remix/app/components/dialogs/passkey-create-dialog.tsx #: apps/remix/app/components/dialogs/document-resend-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx #: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx #: apps/remix/app/components/dialogs/document-delete-dialog.tsx @@ -1480,6 +1535,10 @@ msgstr "Abbrechen" msgid "Cancelled by user" msgstr "Vom Benutzer abgebrochen" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Cannot remove document" +msgstr "" + #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Cannot remove signer" msgstr "Unterzeichner kann nicht entfernt werden" @@ -1533,6 +1592,10 @@ msgstr "Wählen Sie den direkten Link Empfänger" msgid "Choose how the document will reach recipients" msgstr "Wählen Sie, wie das Dokument die Empfänger erreichen soll" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Choose how to distribute your document to recipients. Email will send notifications, None will generate signing links for manual distribution." +msgstr "" + #: apps/remix/app/components/forms/token.tsx msgid "Choose..." msgstr "Wählen..." @@ -1602,6 +1665,10 @@ msgstr "Klicken, um das Feld auszufüllen" msgid "Close" msgstr "Schließen" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Communication" +msgstr "" + #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/forms/signup.tsx @@ -1658,10 +1725,33 @@ msgstr "Abgeschlossene Dokumente" msgid "Completed Documents" msgstr "Abgeschlossene Dokumente" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "Completed on {formattedDate}" +msgstr "" + +#. placeholder {0}: parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[currentField.type]) +#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx +msgid "Configure {0} Field" +msgstr "" + +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Configure additional options and preferences" +msgstr "" + #: packages/lib/constants/template.ts msgid "Configure Direct Recipient" msgstr "Direkten Empfänger konfigurieren" +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Configure Document" +msgstr "" + +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +msgid "Configure Fields" +msgstr "" + #: apps/remix/app/components/general/document/document-edit-form.tsx msgid "Configure general settings for the document." msgstr "Konfigurieren Sie die allgemeinen Einstellungen für das Dokument." @@ -1674,12 +1764,22 @@ msgstr "Konfigurieren Sie die allgemeinen Einstellungen für die Vorlage." msgid "Configure template" msgstr "Vorlage konfigurieren" +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Configure Template" +msgstr "" + #. placeholder {0}: parseMessageDescriptor( _, FRIENDLY_FIELD_TYPE[currentField.type], ) +#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Configure the {0} field" msgstr "Konfigurieren Sie das Feld {0}" +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +msgid "Configure the fields you want to place on the document." +msgstr "" + #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx msgid "Confirm" @@ -1695,6 +1795,8 @@ msgstr "Bestätigen Sie durch Eingabe von <0>{deleteMessage}" #: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx #: apps/remix/app/components/dialogs/token-delete-dialog.tsx +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx msgid "Confirm by typing: <0>{deleteMessage}" msgstr "Bestätigen Sie durch Eingabe: <0>{deleteMessage}" @@ -1724,12 +1826,13 @@ msgid "Content" msgstr "Inhalt" #: apps/remix/app/routes/_unauthenticated+/verify-email.$token.tsx -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx #: apps/remix/app/components/general/document-signing/document-signing-form.tsx +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx #: apps/remix/app/components/dialogs/passkey-create-dialog.tsx #: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx @@ -1940,6 +2043,7 @@ msgstr "Erstellt" msgid "Created At" msgstr "Erstellt am" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Created by" msgstr "Erstellt von" @@ -1989,10 +2093,12 @@ msgstr "Dunkelmodus" msgid "Date" msgstr "Datum" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Date created" msgstr "Erstellungsdatum" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Date Format" @@ -2042,8 +2148,11 @@ msgstr "Löschen" #. placeholder {0}: webhook.webhookUrl #. placeholder {0}: token.name +#. placeholder {0}: folder?.name ?? 'folder' #: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx #: apps/remix/app/components/dialogs/token-delete-dialog.tsx +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx msgid "delete {0}" msgstr "löschen {0}" @@ -2217,6 +2326,10 @@ msgstr "Zeigen Sie Ihren Namen und Ihre E-Mail in Dokumenten an" msgid "Distribute Document" msgstr "Dokument verteilen" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Distribution Method" +msgstr "" + #: apps/remix/app/components/dialogs/template-delete-dialog.tsx msgid "Do you want to delete this template?" msgstr "Möchten Sie diese Vorlage löschen?" @@ -2229,6 +2342,7 @@ msgstr "Möchten Sie diese Vorlage duplizieren?" msgid "Documenso will delete <0>all of your documents, along with all of your completed documents, signatures, and all other resources belonging to your Account." msgstr "Documenso wird <0>alle Ihre Dokumente löschen, zusammen mit allen abgeschlossenen Dokumenten, Unterschriften und allen anderen Ressourcen, die zu Ihrem Konto gehören." +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx msgid "Document" @@ -2294,6 +2408,10 @@ msgstr "Dokument abgeschlossen!" msgid "Document created" msgstr "Dokument erstellt" +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Document Created" +msgstr "" + #. placeholder {0}: document.user.name #: apps/remix/app/components/general/template/template-page-view-recent-activity.tsx msgid "Document created by <0>{0}" @@ -2312,6 +2430,7 @@ msgstr "Dokument erstellt mit einem <0>direkten Link" msgid "Document Creation" msgstr "Dokumenterstellung" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx #: apps/remix/app/components/dialogs/document-delete-dialog.tsx #: apps/remix/app/components/dialogs/admin-document-delete-dialog.tsx @@ -2344,12 +2463,18 @@ msgstr "Dokument dupliziert" msgid "Document external ID updated" msgstr "Externe ID des Dokuments aktualisiert" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "Document found in your account" +msgstr "" + +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx #: apps/remix/app/components/general/document/document-history-sheet.tsx msgid "Document history" msgstr "Dokumentverlauf" #: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Document ID" msgstr "Dokument-ID" @@ -2358,6 +2483,14 @@ msgstr "Dokument-ID" msgid "Document inbox" msgstr "Dokumenten-Posteingang" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Document is already uploaded" +msgstr "" + +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Document is using legacy field insertion" +msgstr "" + #: apps/remix/app/components/tables/templates-table.tsx msgid "Document Limit Exceeded!" msgstr "Dokumentenlimit überschritten!" @@ -2366,6 +2499,7 @@ msgstr "Dokumentenlimit überschritten!" msgid "Document metrics" msgstr "Dokumentmetrik" +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Document moved" msgstr "Dokument verschoben" @@ -2429,10 +2563,12 @@ msgstr "Dokument unterzeichnen Authentifizierung aktualisiert" msgid "Document signing process will be cancelled" msgstr "Der Dokumentenunterzeichnungsprozess wird abgebrochen" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Document status" msgstr "Dokumentenstatus" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Document title" msgstr "Dokumenttitel" @@ -2445,11 +2581,16 @@ msgstr "Dokumenttitel aktualisiert" msgid "Document updated" msgstr "Dokument aktualisiert" +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +msgid "Document updated successfully" +msgstr "" + #: apps/remix/app/components/general/document/document-upload.tsx msgid "Document upload disabled due to unpaid invoices" msgstr "Dokumenten-Upload deaktiviert aufgrund unbezahlter Rechnungen" #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "Document uploaded" msgstr "Dokument hochgeladen" @@ -2466,6 +2607,9 @@ msgid "Document will be permanently deleted" msgstr "Dokument wird dauerhaft gelöscht" #: apps/remix/app/routes/_profile+/p.$url.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId._index.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.edit.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.edit.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx @@ -2480,6 +2624,7 @@ msgstr "Dokument wird dauerhaft gelöscht" msgid "Documents" msgstr "Dokumente" +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Documents created from template" msgstr "Dokumente erstellt aus Vorlage" @@ -2500,6 +2645,7 @@ msgstr "Haben Sie kein Konto? <0>Registrieren" #: apps/remix/app/components/tables/team-settings-billing-invoices-table.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx +#: apps/remix/app/components/general/share-document-download-button.tsx #: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx #: apps/remix/app/components/forms/2fa/view-recovery-codes-dialog.tsx @@ -2517,6 +2663,11 @@ msgstr "Auditprotokolle herunterladen" msgid "Download Certificate" msgstr "Zertifikat herunterladen" +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx +#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx +msgid "Download Original" +msgstr "" + #: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx msgid "Download Template CSV" msgstr "Vorlage CSV herunterladen" @@ -2539,10 +2690,22 @@ msgstr "Entwurfte Dokumente" msgid "Drag & drop your PDF here." msgstr "Ziehen Sie Ihr PDF hierher." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Drag and drop or click to upload" +msgstr "" + +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +msgid "Drag and drop your PDF file here" +msgstr "" + #: packages/lib/constants/document.ts msgid "Draw" msgstr "Zeichnen" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Drop your document here" +msgstr "" + #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Dropdown" @@ -2577,6 +2740,7 @@ msgstr "Duplizieren" msgid "Edit" msgstr "Bearbeiten" +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Edit Template" msgstr "Vorlage bearbeiten" @@ -2607,6 +2771,9 @@ msgstr "Offenlegung der elektronischen Unterschrift" #: apps/remix/app/components/forms/forgot-password.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx @@ -2700,6 +2867,7 @@ msgstr "Aktivieren Sie individuelles Branding für alle Dokumente in diesem Team msgid "Enable Direct Link Signing" msgstr "Direktlink-Signierung aktivieren" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Enable signing order" @@ -2746,6 +2914,10 @@ msgstr "Geben Sie Ihren Namen ein" msgid "Enter your text here" msgstr "Geben Sie hier Ihren Text ein" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx #: apps/remix/app/components/general/verify-email-banner.tsx @@ -2780,10 +2952,15 @@ msgstr "Geben Sie hier Ihren Text ein" #: apps/remix/app/components/general/document/document-edit-form.tsx #: apps/remix/app/components/general/document/document-edit-form.tsx #: apps/remix/app/components/general/document/document-edit-form.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx #: apps/remix/app/components/dialogs/webhook-create-dialog.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx #: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx #: apps/remix/app/components/dialogs/admin-user-enable-dialog.tsx #: apps/remix/app/components/dialogs/admin-user-disable-dialog.tsx @@ -2793,6 +2970,10 @@ msgstr "Geben Sie hier Ihren Text ein" msgid "Error" msgstr "Fehler" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Error uploading file" +msgstr "" + #: apps/remix/app/components/forms/team-document-preferences-form.tsx msgid "Everyone can access and view the document" msgstr "Jeder kann auf das Dokument zugreifen und es anzeigen" @@ -2823,6 +3004,11 @@ msgstr "Läuft ab am {0}" msgid "External ID" msgstr "Externe ID" +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +msgid "Failed to create folder" +msgstr "" + #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx msgid "Failed to reseal document" msgstr "Dokument konnte nicht erneut versiegelt werden" @@ -2831,10 +3017,18 @@ msgstr "Dokument konnte nicht erneut versiegelt werden" msgid "Failed to save settings." msgstr "Einstellungen konnten nicht gespeichert werden." +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +msgid "Failed to update document" +msgstr "" + #: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx msgid "Failed to update recipient" msgstr "Empfänger konnte nicht aktualisiert werden" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +msgid "Failed to update template" +msgstr "" + #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id.tsx #: apps/remix/app/routes/_authenticated+/settings+/webhooks.$id.tsx msgid "Failed to update webhook" @@ -2887,7 +3081,13 @@ msgstr "Feld nicht unterschrieben" msgid "Fields" msgstr "Felder" +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Fields updated" +msgstr "" + #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx msgid "File cannot be larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB" msgstr "Die Datei darf nicht größer als {APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB sein" @@ -2895,6 +3095,25 @@ msgstr "Die Datei darf nicht größer als {APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB se msgid "File size exceeds the limit of {APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB" msgstr "Dateigröße überschreitet das Limit von {APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Folder" +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +msgid "Folder created successfully" +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx +#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx +msgid "Folder not found" +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx +#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx +msgid "Folder updated successfully" +msgstr "" + #: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx #: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx #: packages/ui/primitives/document-flow/field-items-advanced-settings/name-field.tsx @@ -2941,6 +3160,7 @@ msgstr "Vollständiger Name" #: apps/remix/app/components/general/teams/team-settings-nav-desktop.tsx #: apps/remix/app/components/general/document/document-edit-form.tsx #: apps/remix/app/components/general/direct-template/direct-template-page.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx msgid "General" msgstr "Allgemein" @@ -2971,6 +3191,10 @@ msgstr "Zurück nach Hause" msgid "Go Back Home" msgstr "Zurück nach Hause" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "Go to document" +msgstr "" + #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx msgid "Go to owner" msgstr "Zum Eigentümer gehen" @@ -3142,7 +3366,7 @@ msgstr "Ungültiger Code. Bitte versuchen Sie es erneut." msgid "Invalid email" msgstr "Ungültige E-Mail" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx msgid "Invalid link" msgstr "Ungültiger Link" @@ -3239,6 +3463,7 @@ msgid "Label" msgstr "Beschriftung" #: apps/remix/app/components/general/menu-switcher.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Language" @@ -3261,6 +3486,7 @@ msgstr "Die letzten 7 Tage" msgid "Last modified" msgstr "Zuletzt geändert" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Last updated" msgstr "Zuletzt aktualisiert" @@ -3363,6 +3589,7 @@ msgstr "Verwalten Sie das Profil von {0}" msgid "Manage all teams you are currently associated with." msgstr "Verwalten Sie alle Teams, mit denen Sie derzeit verbunden sind." +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Manage and view template" msgstr "Vorlage verwalten und anzeigen" @@ -3471,6 +3698,7 @@ msgstr "Mitglied seit" msgid "Members" msgstr "Mitglieder" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-subject.tsx msgid "Message <0>(Optional)" @@ -3498,19 +3726,38 @@ msgstr "Monatlich aktive Benutzer: Benutzer, die mindestens ein Dokument erstell msgid "Monthly Active Users: Users that had at least one of their documents completed" msgstr "Monatlich aktive Benutzer: Benutzer, die mindestens eines ihrer Dokumente abgeschlossen haben" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Move" msgstr "Verschieben" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "Move \"{templateTitle}\" to a folder" +msgstr "" + +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Move Document to Folder" +msgstr "" + #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Move Document to Team" msgstr "Dokument in Team verschieben" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "Move Template to Folder" +msgstr "" + #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "Move Template to Team" msgstr "Vorlage in Team verschieben" +#: apps/remix/app/components/tables/templates-table-action-dropdown.tsx +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx +msgid "Move to Folder" +msgstr "" + #: apps/remix/app/components/tables/templates-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx msgid "Move to Team" @@ -3534,6 +3781,8 @@ msgstr "Meine Vorlagen" #: apps/remix/app/components/general/claim-account.tsx #: apps/remix/app/components/general/document-signing/document-signing-name-field.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx @@ -3622,8 +3871,8 @@ msgstr "Keine aktuellen Aktivitäten" msgid "No recent documents" msgstr "Keine aktuellen Dokumente" +#: packages/ui/primitives/recipient-selector.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx -#: packages/ui/primitives/document-flow/add-fields.tsx msgid "No recipient matching this description was found." msgstr "Kein passender Empfänger mit dieser Beschreibung gefunden." @@ -3634,8 +3883,8 @@ msgstr "Kein passender Empfänger mit dieser Beschreibung gefunden." msgid "No recipients" msgstr "Keine Empfänger" +#: packages/ui/primitives/recipient-selector.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx -#: packages/ui/primitives/document-flow/add-fields.tsx msgid "No recipients with this role" msgstr "Keine Empfänger mit dieser Rolle" @@ -3677,6 +3926,7 @@ msgstr "Kein Wert gefunden." msgid "No worries, it happens! Enter your email and we'll email you a special link to reset your password." msgstr "Keine Sorge, das passiert! Geben Sie Ihre E-Mail ein, und wir senden Ihnen einen speziellen Link zum Zurücksetzen Ihres Passworts." +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/lib/constants/document.ts msgid "None" msgstr "Keine" @@ -3900,8 +4150,8 @@ msgstr "Zahlung überfällig" #: apps/remix/app/components/tables/user-settings-teams-page-table.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx #: apps/remix/app/components/general/document/document-status.tsx -#: apps/remix/app/components/general/document/document-read-only-fields.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx +#: packages/ui/components/document/document-read-only-fields.tsx #: packages/lib/constants/document.ts msgid "Pending" msgstr "Ausstehend" @@ -3989,6 +4239,11 @@ msgstr "Bitte überprüfe deine E-Mail auf Updates." msgid "Please choose your new password" msgstr "Bitte wählen Sie Ihr neues Passwort" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +msgid "Please configure the document first" +msgstr "" + #: packages/lib/server-only/auth/send-confirmation-email.ts msgid "Please confirm your email" msgstr "Bitte bestätige deine E-Mail" @@ -4261,6 +4516,7 @@ msgstr "Empfänger aktualisiert" #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx #: apps/remix/app/components/general/template/template-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx msgid "Recipients" msgstr "Empfänger" @@ -4284,6 +4540,7 @@ msgstr "Wiederherstellungscodes" msgid "Red" msgstr "Rot" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Redirect URL" @@ -4345,7 +4602,6 @@ msgstr "Erinnerung: Bitte {recipientActionVerb} dein Dokument" #: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx #: apps/remix/app/components/general/teams/team-email-dropdown.tsx #: apps/remix/app/components/general/document-signing/document-signing-field-container.tsx -#: apps/remix/app/components/general/document-signing/document-signing-field-container.tsx #: apps/remix/app/components/forms/team-branding-preferences-form.tsx #: apps/remix/app/components/forms/avatar-image.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx @@ -4442,7 +4698,7 @@ msgstr "Aufbewahrung von Dokumenten" msgid "Retry" msgstr "Wiederholen" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.decline.$token.tsx @@ -4473,6 +4729,7 @@ msgstr "Zugriff widerrufen" #: apps/remix/app/components/tables/user-settings-current-teams-table.tsx #: apps/remix/app/components/tables/team-settings-members-table.tsx #: apps/remix/app/components/tables/team-settings-member-invites-table.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx #: apps/remix/app/components/dialogs/team-member-invite-dialog.tsx @@ -4484,12 +4741,19 @@ msgstr "Rolle" msgid "Roles" msgstr "Rollen" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Root (No Folder)" +msgstr "" + #: packages/ui/primitives/data-table-pagination.tsx msgid "Rows per page" msgstr "Zeilen pro Seite" #: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx #: apps/remix/app/components/general/document-signing/document-signing-number-field.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx msgid "Save" @@ -4544,6 +4808,10 @@ msgstr "Sicherheitsaktivität" msgid "Select" msgstr "Auswählen" +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Select a folder to move this document to." +msgstr "" + #: apps/remix/app/components/dialogs/template-move-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Select a team" @@ -4665,6 +4933,14 @@ msgstr "Gesendet" msgid "Set a password" msgstr "Ein Passwort festlegen" +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Set up your document properties and recipient information" +msgstr "" + +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Set up your template properties and recipient information" +msgstr "" + #: apps/remix/app/routes/_authenticated+/settings+/_layout.tsx #: apps/remix/app/components/general/app-nav-mobile.tsx #: apps/remix/app/components/general/app-command-menu.tsx @@ -4806,7 +5082,6 @@ msgstr "Registrieren mit OIDC" #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/types.ts -#: packages/ui/primitives/document-flow/field-icon.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Signature" msgstr "Unterschrift" @@ -4836,8 +5111,8 @@ msgid "Signatures will appear once the document has been completed" msgstr "Unterschriften erscheinen, sobald das Dokument abgeschlossen ist" #: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx -#: apps/remix/app/components/general/document/document-read-only-fields.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx +#: packages/ui/components/document/document-read-only-fields.tsx #: packages/lib/constants/recipient-roles.ts msgid "Signed" msgstr "Unterzeichnet" @@ -4934,7 +5209,9 @@ msgstr "Einige Unterzeichner haben noch kein Unterschriftsfeld zugewiesen bekomm #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx +#: apps/remix/app/components/general/share-document-download-button.tsx #: apps/remix/app/components/general/billing-portal-button.tsx #: apps/remix/app/components/general/billing-plans.tsx #: apps/remix/app/components/general/teams/team-layout-billing-banner.tsx @@ -4946,6 +5223,7 @@ msgstr "Einige Unterzeichner haben noch kein Unterschriftsfeld zugewiesen bekomm #: apps/remix/app/components/general/document-signing/document-signing-auth-page.tsx #: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx #: apps/remix/app/components/general/document/document-page-view-dropdown.tsx +#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx #: apps/remix/app/components/general/document/document-certificate-download-button.tsx #: apps/remix/app/components/general/document/document-audit-log-download-button.tsx @@ -4972,7 +5250,7 @@ msgid "Something went wrong" msgstr "Etwas ist schief gelaufen" #. placeholder {0}: data.teamName -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Something went wrong while attempting to transfer the ownership of team <0>{0} to your. Please try again later or contact support." msgstr "Etwas ist schiefgelaufen beim Versuch, das Eigentum des Teams <0>{0} auf Ihre zu übertragen. Bitte versuchen Sie es später erneut oder kontaktieren Sie den Support." @@ -5044,6 +5322,7 @@ msgstr "Status" msgid "Step <0>{step} of {maxStep}" msgstr "Schritt <0>{step} von {maxStep}" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-subject.tsx msgid "Subject <0>(Optional)" @@ -5061,6 +5340,8 @@ msgstr "Abonnement" msgid "Subscriptions" msgstr "Abonnements" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx #: apps/remix/app/components/tables/user-settings-pending-teams-table-actions.tsx #: apps/remix/app/components/tables/team-settings-member-invites-table.tsx @@ -5201,15 +5482,15 @@ msgstr "Nur Team" msgid "Team only templates are not linked anywhere and are visible only to your team." msgstr "Nur Teamvorlagen sind nirgendwo verlinkt und nur für Ihr Team sichtbar." -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Team ownership transfer" msgstr "Team-Eigentumsübertragung" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Team ownership transfer already completed!" msgstr "Team-Eigentumsübertragung bereits abgeschlossen!" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Team ownership transferred!" msgstr "Team-Eigentum übertragen!" @@ -5258,6 +5539,8 @@ msgstr "Teams" msgid "Teams restricted" msgstr "Teams beschränkt" +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id.edit.tsx +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id.edit.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx @@ -5268,6 +5551,10 @@ msgstr "Teams beschränkt" msgid "Template" msgstr "Vorlage" +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Template Created" +msgstr "" + #: apps/remix/app/components/dialogs/template-delete-dialog.tsx msgid "Template deleted" msgstr "Vorlage gelöscht" @@ -5288,6 +5575,11 @@ msgstr "Vorlage ist von Deinem öffentlichen Profil entfernt worden." msgid "Template has been updated." msgstr "Vorlage wurde aktualisiert." +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Template is using legacy field insertion" +msgstr "" + +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "Template moved" msgstr "Vorlage verschoben" @@ -5304,6 +5596,12 @@ msgstr "Vorlage gespeichert" msgid "Template title" msgstr "Vorlagentitel" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +msgid "Template updated successfully" +msgstr "" + +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx #: apps/remix/app/components/general/app-nav-mobile.tsx @@ -5379,10 +5677,18 @@ msgstr "Der Inhalt, der im Banne rgezeig wird, HTML ist erlaubt" msgid "The direct link has been copied to your clipboard" msgstr "Der direkte Linkt wurde in die Zwischenablage kopiert" +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "The document has been moved successfully." +msgstr "" + #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "The document has been successfully moved to the selected team." msgstr "Das Dokument wurde erfolgreich in das ausgewählte Team verschoben." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "The document is already saved and cannot be changed." +msgstr "" + #: apps/remix/app/components/embed/embed-document-completed.tsx msgid "The document is now completed, please follow any instructions provided within the parent application." msgstr "Das Dokument ist jetzt abgeschlossen. Bitte folgen Sie allen Anweisungen, die in der übergeordneten Anwendung bereitgestellt werden." @@ -5421,6 +5727,28 @@ msgstr "Die angegebene E-Mail oder das Passwort ist falsch" msgid "The events that will trigger a webhook to be sent to your URL." msgstr "Die Ereignisse, die einen Webhook auslösen, der an Ihre URL gesendet wird." +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "The fields have been updated to the new field insertion method successfully" +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx +msgid "The folder you are trying to delete does not exist." +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx +#: apps/remix/app/components/dialogs/folder-move-dialog.tsx +msgid "The folder you are trying to move does not exist." +msgstr "" + +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "The folder you are trying to move the document to does not exist." +msgstr "" + +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "The folder you are trying to move the template to does not exist." +msgstr "" + #: packages/email/templates/bulk-send-complete.tsx msgid "The following errors occurred:" msgstr "Die folgenden Fehler sind aufgetreten:" @@ -5434,7 +5762,7 @@ msgid "The following team has been deleted by you" msgstr "Das folgende Team wurde von dir gelöscht" #. placeholder {0}: data.teamName -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "The ownership of team <0>{0} has been successfully transferred to you." msgstr "Die Inhaberschaft des Teams <0>{0} wurde erfolgreich auf Sie übertragen." @@ -5531,10 +5859,15 @@ msgid "The team transfer request to <0>{0} has expired." msgstr "Die Teamübertragungsanfrage an <0>{0} ist abgelaufen." #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/_layout.tsx -msgid "The team you are looking for may have been removed, renamed or may have never\n" +msgid "" +"The team you are looking for may have been removed, renamed or may have never\n" " existed." msgstr "Das Team, das Sie suchen, könnte entfernt, umbenannt oder nie existiert haben." +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "The template has been moved successfully." +msgstr "" + #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "The template has been successfully moved to the selected team." msgstr "Die Vorlage wurde erfolgreich in das ausgewählte Team verschoben." @@ -5590,6 +5923,10 @@ msgstr "Es gibt derzeit keine aktiven Entwürfe. Sie können ein Dokument hochla msgid "There are no completed documents yet. Documents that you have created or received will appear here once completed." msgstr "Es gibt noch keine abgeschlossenen Dokumente. Dokumente, die Sie erstellt oder erhalten haben, werden hier angezeigt, sobald sie abgeschlossen sind." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "There was an error uploading your file. Please try again." +msgstr "" + #: apps/remix/app/components/general/teams/team-email-usage.tsx msgid "They have permission on your behalf to:" msgstr "Sie haben in Ihrem Namen die Erlaubnis, zu:" @@ -5620,6 +5957,10 @@ msgstr "Dies kann überschrieben werden, indem die Authentifizierungsanforderung msgid "This document can not be recovered, if you would like to dispute the reason for future documents please contact support." msgstr "Dieses Dokument kann nicht wiederhergestellt werden. Wenn du den Grund für zukünftige Dokumente anfechten möchtest, kontaktiere bitte den Support." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "This document cannot be changed" +msgstr "" + #: apps/remix/app/components/dialogs/document-delete-dialog.tsx msgid "This document could not be deleted at this time. Please try again." msgstr "Dieses Dokument konnte derzeit nicht gelöscht werden. Bitte versuchen Sie es erneut." @@ -5632,7 +5973,7 @@ msgstr "Dieses Dokument konnte derzeit nicht dupliziert werden. Bitte versuche e msgid "This document could not be re-sent at this time. Please try again." msgstr "Dieses Dokument konnte zu diesem Zeitpunkt nicht erneut gesendet werden. Bitte versuchen Sie es erneut." -#: packages/ui/primitives/document-flow/add-fields.tsx +#: packages/ui/primitives/recipient-selector.tsx msgid "This document has already been sent to this recipient. You can no longer edit this recipient." msgstr "Dieses Dokument wurde bereits an diesen Empfänger gesendet. Sie können diesen Empfänger nicht mehr bearbeiten." @@ -5644,18 +5985,29 @@ msgstr "Dieses Dokument wurde vom Eigentümer storniert und steht anderen nicht msgid "This document has been cancelled by the owner." msgstr "Dieses Dokument wurde vom Eigentümer storniert." +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "This document has been rejected by a recipient" msgstr "Dieses Dokument wurde von einem Empfänger abgelehnt" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "This document has been signed by all recipients" msgstr "Dieses Dokument wurde von allen Empfängern unterschrieben" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "This document is available in your Documenso account. You can view more details, recipients, and audit logs there." +msgstr "" + +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "This document is currently a draft and has not been sent" msgstr "Dieses Dokument ist momentan ein Entwurf und wurde nicht gesendet" +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "This document is using legacy field insertion, we recommend using the new field insertion method for more accurate results." +msgstr "" + #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx msgid "This document was created by you or a team member using the template above." msgstr "Dieses Dokument wurde von Ihnen oder einem Teammitglied unter Verwendung der oben genannten Vorlage erstellt." @@ -5696,11 +6048,16 @@ msgstr "Diese E-Mail wird an den Empfänger gesendet, der das Dokument gerade un msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them." msgstr "Dieses Feld kann nicht geändert oder gelöscht werden. Wenn Sie den direkten Link dieser Vorlage teilen oder zu Ihrem öffentlichen Profil hinzufügen, kann jeder, der darauf zugreift, seinen Namen und seine E-Mail-Adresse eingeben und die ihm zugewiesenen Felder ausfüllen." +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +#: apps/remix/app/components/dialogs/folder-create-dialog.tsx +msgid "This folder name is already taken." +msgstr "" + #: packages/ui/primitives/template-flow/add-template-settings.tsx msgid "This is how the document will reach the recipients once the document is ready for signing." msgstr "So wird das Dokument die Empfänger erreichen, sobald es zum Unterschreiben bereit ist." -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "This link is invalid or has expired. Please contact your team to resend a transfer request." msgstr "Dieser Link ist ungültig oder abgelaufen. Bitte kontaktieren Sie Ihr Team, um eine erneute Überweisungsanfrage zu senden." @@ -5740,6 +6097,10 @@ msgstr "Dieses Team und alle zugehörigen Daten, ausgenommen Rechnungen, werden msgid "This template could not be deleted at this time. Please try again." msgstr "Diese Vorlage konnte derzeit nicht gelöscht werden. Bitte versuchen Sie es erneut." +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "This template is using legacy field insertion, we recommend using the new field insertion method for more accurate results." +msgstr "" + #: apps/remix/app/routes/_unauthenticated+/team.decline.$token.tsx msgid "This token is invalid or has expired. No action is needed." msgstr "Dieses Token ist ungültig oder abgelaufen. Es sind keine weiteren Maßnahmen erforderlich." @@ -5778,11 +6139,13 @@ msgstr "Dies überschreibt alle globalen Einstellungen." msgid "Time" msgstr "Zeit" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Time zone" msgstr "Zeitzone" #: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Time Zone" @@ -5792,6 +6155,7 @@ msgstr "Zeitzone" #: apps/remix/app/components/tables/templates-table.tsx #: apps/remix/app/components/tables/documents-table.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Title" msgstr "Titel" @@ -6101,6 +6465,10 @@ msgstr "Aktualisieren" msgid "Update Banner" msgstr "Banner aktualisieren" +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Update Fields" +msgstr "" + #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx msgid "Update passkey" msgstr "Passkey aktualisieren" @@ -6115,6 +6483,7 @@ msgstr "Profil aktualisieren" #: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx +#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx msgid "Update Recipient" msgstr "Empfänger aktualisieren" @@ -6157,10 +6526,15 @@ msgstr "Passwort wird aktualisiert..." msgid "Updating Your Information" msgstr "Aktualisierung Ihrer Informationen" +#: packages/ui/primitives/document-upload.tsx #: packages/ui/primitives/document-dropzone.tsx msgid "Upgrade" msgstr "Upgrade" +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +msgid "Upgrade your plan to upload more documents" +msgstr "" + #: packages/lib/constants/document.ts msgid "Upload" msgstr "Hochladen" @@ -6189,10 +6563,17 @@ msgstr "CSV hochladen" msgid "Upload custom document" msgstr "Benutzerdefiniertes Dokument hochladen" +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +#: packages/ui/primitives/document-upload.tsx +msgid "Upload Document" +msgstr "" + #: packages/ui/primitives/signature-pad/signature-pad-upload.tsx msgid "Upload Signature" msgstr "Signatur hochladen" +#: packages/ui/primitives/document-upload.tsx #: packages/ui/primitives/document-dropzone.tsx msgid "Upload Template Document" msgstr "Vorlagendokument hochladen" @@ -6218,6 +6599,11 @@ msgstr "Die hochgeladene Datei ist zu klein" msgid "Uploaded file not an allowed file type" msgstr "Die hochgeladene Datei ist kein zulässiger Dateityp" +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +msgid "Uploading document..." +msgstr "" + +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Use" msgstr "Verwenden" @@ -6666,6 +7052,7 @@ msgstr "Wir werden Unterzeichnungslinks für Sie erstellen, die Sie an die Empf msgid "We won't send anything to notify recipients." msgstr "Wir werden nichts senden, um die Empfänger zu benachrichtigen." +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx #: apps/remix/app/routes/_authenticated+/templates._index.tsx #: apps/remix/app/components/tables/documents-table-empty-state.tsx msgid "We're all empty" @@ -6730,6 +7117,7 @@ msgstr "Willkommen bei Documenso!" msgid "Were you trying to edit this document instead?" msgstr "Hast du stattdessen versucht, dieses Dokument zu bearbeiten?" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "When enabled, signers can choose who should sign next in the sequence instead of following the predefined order." @@ -6889,11 +7277,13 @@ msgstr "Sie dürfen nicht mehr als {MAXIMUM_PASSKEYS} Passkeys haben." msgid "You cannot modify a team member who has a higher role than you." msgstr "Sie können ein Teammitglied, das eine höhere Rolle als Sie hat, nicht ändern." +#: packages/ui/primitives/document-upload.tsx #: packages/ui/primitives/document-dropzone.tsx msgid "You cannot upload documents at this time." msgstr "Sie können derzeit keine Dokumente hochladen." #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "You cannot upload encrypted PDFs" msgstr "Sie können keine verschlüsselten PDFs hochladen" @@ -6915,7 +7305,7 @@ msgid "You have accepted an invitation from <0>{0} to join their team." msgstr "Sie haben eine Einladung von <0>{0} angenommen, um ihrem Team beizutreten." #. placeholder {0}: data.teamName -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "You have already completed the ownership transfer for <0>{0}." msgstr "Sie haben die Eigentumsübertragung für <0>{0} bereits abgeschlossen." @@ -6965,6 +7355,7 @@ msgstr "Du hast das Dokument {0} initiiert, das erfordert, dass du {recipientAct msgid "You have no webhooks yet. Your webhooks will be shown here once you create them." msgstr "Sie haben noch keine Webhooks. Ihre Webhooks werden hier angezeigt, sobald Sie sie erstellt haben." +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx #: apps/remix/app/routes/_authenticated+/templates._index.tsx msgid "You have not yet created any templates. To create a template please upload one." msgstr "Sie haben noch keine Vorlagen erstellt. Bitte laden Sie eine Datei hoch, um eine Vorlage zu erstellen." @@ -6979,6 +7370,7 @@ msgid "You have reached the maximum limit of {0} direct templates. <0>Upgrade yo msgstr "Sie haben das maximale Limit von {0} direkten Vorlagen erreicht. <0>Upgrade your account to continue!" #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "You have reached your document limit for this month. Please upgrade your plan." msgstr "Sie haben Ihr Dokumentenlimit für diesen Monat erreicht. Bitte aktualisieren Sie Ihren Plan." @@ -7052,6 +7444,11 @@ msgstr "Sie müssen mindestens einen anderen Teamkollegen haben, um die Eigentum msgid "You must set a profile URL before enabling your public profile." msgstr "Sie müssen eine Profil-URL festlegen, bevor Sie Ihr öffentliches Profil aktivieren." +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx +msgid "You must type '{deleteMessage}' to confirm" +msgstr "" + #: apps/remix/app/routes/_authenticated+/settings+/tokens.tsx msgid "You need to be an admin to manage API tokens." msgstr "Sie müssen Administrator sein, um API-Token zu verwalten." @@ -7117,6 +7514,8 @@ msgid "Your direct signing templates" msgstr "Ihre direkten Unterzeichnungsvorlagen" #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx msgid "Your document failed to upload." msgstr "Ihr Dokument konnte nicht hochgeladen werden." @@ -7124,6 +7523,10 @@ msgstr "Ihr Dokument konnte nicht hochgeladen werden." msgid "Your document has been created from the template successfully." msgstr "Ihr Dokument wurde erfolgreich aus der Vorlage erstellt." +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Your document has been created successfully" +msgstr "" + #: packages/email/template-components/template-document-super-delete.tsx msgid "Your document has been deleted by an admin!" msgstr "Dein Dokument wurde von einem Administrator gelöscht!" @@ -7141,6 +7544,7 @@ msgid "Your document has been successfully duplicated." msgstr "Ihr Dokument wurde erfolgreich dupliziert." #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "Your document has been uploaded successfully." msgstr "Ihr Dokument wurde erfolgreich hochgeladen." @@ -7234,6 +7638,10 @@ msgstr "Ihr Team wurde erfolgreich gelöscht." msgid "Your team has been successfully updated." msgstr "Ihr Team wurde erfolgreich aktualisiert." +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Your template has been created successfully" +msgstr "" + #: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx msgid "Your template has been duplicated successfully." msgstr "Ihre Vorlage wurde erfolgreich dupliziert." @@ -7261,4 +7669,3 @@ msgstr "Ihr Token wurde erfolgreich erstellt! Stellen Sie sicher, dass Sie es ko #: apps/remix/app/routes/_authenticated+/settings+/tokens.tsx msgid "Your tokens will be shown here once you create them." msgstr "Ihre Tokens werden hier angezeigt, sobald Sie sie erstellt haben." - diff --git a/packages/lib/translations/en/web.po b/packages/lib/translations/en/web.po index 7e4ba7ca8..a41f89a1d 100644 --- a/packages/lib/translations/en/web.po +++ b/packages/lib/translations/en/web.po @@ -21,6 +21,10 @@ msgstr " Enable direct link signing" msgid " The events that will trigger a webhook to be sent to your URL." msgstr " The events that will trigger a webhook to be sent to your URL." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid ".PDF documents accepted (max {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB)" +msgstr ".PDF documents accepted (max {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB)" + #. placeholder {0}: team.name #: apps/remix/app/components/forms/team-document-preferences-form.tsx msgid "\"{0}\" has invited you to sign \"example document\"." @@ -91,11 +95,13 @@ msgid "{0, plural, one {1 matching field} other {# matching fields}}" msgstr "{0, plural, one {1 matching field} other {# matching fields}}" #. placeholder {0}: recipients.length +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.edit.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.edit.tsx msgid "{0, plural, one {1 Recipient} other {# Recipients}}" msgstr "{0, plural, one {1 Recipient} other {# Recipients}}" #. placeholder {0}: pendingRecipients.length +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}" msgstr "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}" @@ -139,6 +145,7 @@ msgstr "{0} left the team {teamName} on Documenso" #. placeholder {0}: remaining.documents #. placeholder {1}: quota.documents #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "{0} of {1} documents remaining this month." msgstr "{0} of {1} documents remaining this month." @@ -156,6 +163,7 @@ msgid "{0} on behalf of \"{1}\" has invited you to {recipientActionVerb} the doc msgstr "{0} on behalf of \"{1}\" has invited you to {recipientActionVerb} the document \"{2}\"." #. placeholder {0}: recipients.length +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "{0} Recipient(s)" msgstr "{0} Recipient(s)" @@ -322,6 +330,10 @@ msgstr "{recipientActionVerb} document" msgid "{recipientActionVerb} the document to complete the process." msgstr "{recipientActionVerb} the document to complete the process." +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "{recipientCount} recipients" +msgstr "{recipientCount} recipients" + #: packages/email/templates/document-created-from-direct-template.tsx msgid "{recipientName} {action} a document by using one of your direct links" msgstr "{recipientName} {action} a document by using one of your direct links" @@ -722,6 +734,7 @@ msgstr "Add" msgid "Add a document" msgstr "Add a document" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Add a URL to redirect the user to once the document is signed" @@ -795,6 +808,7 @@ msgstr "Add Placeholder Recipient" msgid "Add Placeholders" msgstr "Add Placeholders" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Add Signer" msgstr "Add Signer" @@ -803,6 +817,10 @@ msgstr "Add Signer" msgid "Add Signers" msgstr "Add Signers" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +msgid "Add signers and configure signing preferences" +msgstr "Add signers and configure signing preferences" + #: apps/remix/app/components/dialogs/team-email-add-dialog.tsx msgid "Add team email" msgstr "Add team email" @@ -848,11 +866,16 @@ msgstr "Admin panel" msgid "Advanced Options" msgstr "Advanced Options" +#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Advanced settings" msgstr "Advanced settings" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Advanced Settings" +msgstr "Advanced Settings" + #: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx msgid "After signing a document electronically, you will be provided the opportunity to view, download, and print the document for your records. It is highly recommended that you retain a copy of all electronically signed documents for your personal records. We will also retain a copy of the signed document for our records however we may not be able to provide you with a copy of the signed document after a certain period of time." msgstr "After signing a document electronically, you will be provided the opportunity to view, download, and print the document for your records. It is highly recommended that you retain a copy of all electronically signed documents for your personal records. We will also retain a copy of the signed document for our records however we may not be able to provide you with a copy of the signed document after a certain period of time." @@ -877,6 +900,11 @@ msgstr "All documents have been processed. Any new documents that are sent or re msgid "All documents related to the electronic signing process will be provided to you electronically through our platform or via email. It is your responsibility to ensure that your email address is current and that you can receive and open our emails." msgstr "All documents related to the electronic signing process will be provided to you electronically through our platform or via email. It is your responsibility to ensure that your email address is current and that you can receive and open our emails." +#: apps/remix/app/routes/_authenticated+/templates.folders._index.tsx +#: apps/remix/app/routes/_authenticated+/documents.folders._index.tsx +msgid "All Folders" +msgstr "All Folders" + #: apps/remix/app/components/dialogs/document-delete-dialog.tsx msgid "All inserted signatures will be voided" msgstr "All inserted signatures will be voided" @@ -905,11 +933,13 @@ msgstr "All Time" msgid "Allow document recipients to reply directly to this email address" msgstr "Allow document recipients to reply directly to this email address" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Allow signers to dictate next signer" msgstr "Allow signers to dictate next signer" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Allowed Signature Types" @@ -993,8 +1023,11 @@ msgstr "An error occurred while disabling direct link signing." msgid "An error occurred while disabling the user." msgstr "An error occurred while disabling the user." +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx +#: apps/remix/app/components/general/share-document-download-button.tsx +#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx msgid "An error occurred while downloading your document." @@ -1020,10 +1053,12 @@ msgstr "An error occurred while loading team members. Please try again later." msgid "An error occurred while loading the document." msgstr "An error occurred while loading the document." +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "An error occurred while moving the document." msgstr "An error occurred while moving the document." +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "An error occurred while moving the template." msgstr "An error occurred while moving the template." @@ -1098,6 +1133,7 @@ msgid "An error occurred while updating your profile." msgstr "An error occurred while updating your profile." #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "An error occurred while uploading your document." msgstr "An error occurred while uploading your document." @@ -1135,6 +1171,21 @@ msgstr "An unexpected error occurred." msgid "An unknown error occurred" msgstr "An unknown error occurred" +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +#: apps/remix/app/components/dialogs/folder-create-dialog.tsx +msgid "An unknown error occurred while creating the folder." +msgstr "An unknown error occurred while creating the folder." + +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx +msgid "An unknown error occurred while deleting the folder." +msgstr "An unknown error occurred while deleting the folder." + +#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx +#: apps/remix/app/components/dialogs/folder-move-dialog.tsx +msgid "An unknown error occurred while moving the folder." +msgstr "An unknown error occurred while moving the folder." + #: apps/remix/app/components/dialogs/team-transfer-dialog.tsx msgid "Any payment methods attached to this team will remain attached to this team. Please contact us if you need to update this information." msgstr "Any payment methods attached to this team will remain attached to this team. Please contact us if you need to update this information." @@ -1292,6 +1343,8 @@ msgstr "Awaiting email confirmation" #: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx #: apps/remix/app/components/forms/signup.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx msgid "Back" msgstr "Back" @@ -1442,6 +1495,7 @@ msgstr "Can prepare" #: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx #: apps/remix/app/components/dialogs/webhook-create-dialog.tsx #: apps/remix/app/components/dialogs/token-delete-dialog.tsx +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx #: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx @@ -1461,6 +1515,7 @@ msgstr "Can prepare" #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx #: apps/remix/app/components/dialogs/passkey-create-dialog.tsx #: apps/remix/app/components/dialogs/document-resend-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx #: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx #: apps/remix/app/components/dialogs/document-delete-dialog.tsx @@ -1475,6 +1530,10 @@ msgstr "Cancel" msgid "Cancelled by user" msgstr "Cancelled by user" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Cannot remove document" +msgstr "Cannot remove document" + #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Cannot remove signer" msgstr "Cannot remove signer" @@ -1528,6 +1587,10 @@ msgstr "Choose Direct Link Recipient" msgid "Choose how the document will reach recipients" msgstr "Choose how the document will reach recipients" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Choose how to distribute your document to recipients. Email will send notifications, None will generate signing links for manual distribution." +msgstr "Choose how to distribute your document to recipients. Email will send notifications, None will generate signing links for manual distribution." + #: apps/remix/app/components/forms/token.tsx msgid "Choose..." msgstr "Choose..." @@ -1597,6 +1660,10 @@ msgstr "Click to insert field" msgid "Close" msgstr "Close" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Communication" +msgstr "Communication" + #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/forms/signup.tsx @@ -1653,10 +1720,33 @@ msgstr "Completed documents" msgid "Completed Documents" msgstr "Completed Documents" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "Completed on {formattedDate}" +msgstr "Completed on {formattedDate}" + +#. placeholder {0}: parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[currentField.type]) +#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx +msgid "Configure {0} Field" +msgstr "Configure {0} Field" + +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Configure additional options and preferences" +msgstr "Configure additional options and preferences" + #: packages/lib/constants/template.ts msgid "Configure Direct Recipient" msgstr "Configure Direct Recipient" +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Configure Document" +msgstr "Configure Document" + +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +msgid "Configure Fields" +msgstr "Configure Fields" + #: apps/remix/app/components/general/document/document-edit-form.tsx msgid "Configure general settings for the document." msgstr "Configure general settings for the document." @@ -1669,12 +1759,22 @@ msgstr "Configure general settings for the template." msgid "Configure template" msgstr "Configure template" +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Configure Template" +msgstr "Configure Template" + #. placeholder {0}: parseMessageDescriptor( _, FRIENDLY_FIELD_TYPE[currentField.type], ) +#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Configure the {0} field" msgstr "Configure the {0} field" +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +msgid "Configure the fields you want to place on the document." +msgstr "Configure the fields you want to place on the document." + #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx msgid "Confirm" @@ -1690,6 +1790,8 @@ msgstr "Confirm by typing <0>{deleteMessage}" #: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx #: apps/remix/app/components/dialogs/token-delete-dialog.tsx +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx msgid "Confirm by typing: <0>{deleteMessage}" msgstr "Confirm by typing: <0>{deleteMessage}" @@ -1719,12 +1821,13 @@ msgid "Content" msgstr "Content" #: apps/remix/app/routes/_unauthenticated+/verify-email.$token.tsx -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx #: apps/remix/app/components/general/document-signing/document-signing-form.tsx +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx #: apps/remix/app/components/dialogs/passkey-create-dialog.tsx #: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx @@ -1935,6 +2038,7 @@ msgstr "Created" msgid "Created At" msgstr "Created At" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Created by" msgstr "Created by" @@ -1984,10 +2088,12 @@ msgstr "Dark Mode" msgid "Date" msgstr "Date" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Date created" msgstr "Date created" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Date Format" @@ -2037,8 +2143,11 @@ msgstr "Delete" #. placeholder {0}: webhook.webhookUrl #. placeholder {0}: token.name +#. placeholder {0}: folder?.name ?? 'folder' #: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx #: apps/remix/app/components/dialogs/token-delete-dialog.tsx +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx msgid "delete {0}" msgstr "delete {0}" @@ -2212,6 +2321,10 @@ msgstr "Display your name and email in documents" msgid "Distribute Document" msgstr "Distribute Document" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Distribution Method" +msgstr "Distribution Method" + #: apps/remix/app/components/dialogs/template-delete-dialog.tsx msgid "Do you want to delete this template?" msgstr "Do you want to delete this template?" @@ -2224,6 +2337,7 @@ msgstr "Do you want to duplicate this template?" msgid "Documenso will delete <0>all of your documents, along with all of your completed documents, signatures, and all other resources belonging to your Account." msgstr "Documenso will delete <0>all of your documents, along with all of your completed documents, signatures, and all other resources belonging to your Account." +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx msgid "Document" @@ -2289,6 +2403,10 @@ msgstr "Document Completed!" msgid "Document created" msgstr "Document created" +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Document Created" +msgstr "Document Created" + #. placeholder {0}: document.user.name #: apps/remix/app/components/general/template/template-page-view-recent-activity.tsx msgid "Document created by <0>{0}" @@ -2307,6 +2425,7 @@ msgstr "Document created using a <0>direct link" msgid "Document Creation" msgstr "Document Creation" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx #: apps/remix/app/components/dialogs/document-delete-dialog.tsx #: apps/remix/app/components/dialogs/admin-document-delete-dialog.tsx @@ -2339,12 +2458,18 @@ msgstr "Document Duplicated" msgid "Document external ID updated" msgstr "Document external ID updated" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "Document found in your account" +msgstr "Document found in your account" + +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx #: apps/remix/app/components/general/document/document-history-sheet.tsx msgid "Document history" msgstr "Document history" #: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Document ID" msgstr "Document ID" @@ -2353,6 +2478,14 @@ msgstr "Document ID" msgid "Document inbox" msgstr "Document inbox" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Document is already uploaded" +msgstr "Document is already uploaded" + +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Document is using legacy field insertion" +msgstr "Document is using legacy field insertion" + #: apps/remix/app/components/tables/templates-table.tsx msgid "Document Limit Exceeded!" msgstr "Document Limit Exceeded!" @@ -2361,6 +2494,7 @@ msgstr "Document Limit Exceeded!" msgid "Document metrics" msgstr "Document metrics" +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Document moved" msgstr "Document moved" @@ -2424,10 +2558,12 @@ msgstr "Document signing auth updated" msgid "Document signing process will be cancelled" msgstr "Document signing process will be cancelled" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Document status" msgstr "Document status" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Document title" msgstr "Document title" @@ -2440,11 +2576,16 @@ msgstr "Document title updated" msgid "Document updated" msgstr "Document updated" +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +msgid "Document updated successfully" +msgstr "Document updated successfully" + #: apps/remix/app/components/general/document/document-upload.tsx msgid "Document upload disabled due to unpaid invoices" msgstr "Document upload disabled due to unpaid invoices" #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "Document uploaded" msgstr "Document uploaded" @@ -2461,6 +2602,9 @@ msgid "Document will be permanently deleted" msgstr "Document will be permanently deleted" #: apps/remix/app/routes/_profile+/p.$url.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId._index.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.edit.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.edit.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx @@ -2475,6 +2619,7 @@ msgstr "Document will be permanently deleted" msgid "Documents" msgstr "Documents" +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Documents created from template" msgstr "Documents created from template" @@ -2495,6 +2640,7 @@ msgstr "Don't have an account? <0>Sign up" #: apps/remix/app/components/tables/team-settings-billing-invoices-table.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx +#: apps/remix/app/components/general/share-document-download-button.tsx #: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx #: apps/remix/app/components/forms/2fa/view-recovery-codes-dialog.tsx @@ -2512,6 +2658,11 @@ msgstr "Download Audit Logs" msgid "Download Certificate" msgstr "Download Certificate" +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx +#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx +msgid "Download Original" +msgstr "Download Original" + #: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx msgid "Download Template CSV" msgstr "Download Template CSV" @@ -2534,10 +2685,22 @@ msgstr "Drafted Documents" msgid "Drag & drop your PDF here." msgstr "Drag & drop your PDF here." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Drag and drop or click to upload" +msgstr "Drag and drop or click to upload" + +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +msgid "Drag and drop your PDF file here" +msgstr "Drag and drop your PDF file here" + #: packages/lib/constants/document.ts msgid "Draw" msgstr "Draw" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Drop your document here" +msgstr "Drop your document here" + #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Dropdown" @@ -2572,6 +2735,7 @@ msgstr "Duplicate" msgid "Edit" msgstr "Edit" +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Edit Template" msgstr "Edit Template" @@ -2602,6 +2766,9 @@ msgstr "Electronic Signature Disclosure" #: apps/remix/app/components/forms/forgot-password.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx @@ -2695,6 +2862,7 @@ msgstr "Enable custom branding for all documents in this team." msgid "Enable Direct Link Signing" msgstr "Enable Direct Link Signing" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Enable signing order" @@ -2741,6 +2909,10 @@ msgstr "Enter your name" msgid "Enter your text here" msgstr "Enter your text here" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx #: apps/remix/app/components/general/verify-email-banner.tsx @@ -2775,10 +2947,15 @@ msgstr "Enter your text here" #: apps/remix/app/components/general/document/document-edit-form.tsx #: apps/remix/app/components/general/document/document-edit-form.tsx #: apps/remix/app/components/general/document/document-edit-form.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx #: apps/remix/app/components/dialogs/webhook-create-dialog.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx #: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx #: apps/remix/app/components/dialogs/admin-user-enable-dialog.tsx #: apps/remix/app/components/dialogs/admin-user-disable-dialog.tsx @@ -2788,6 +2965,10 @@ msgstr "Enter your text here" msgid "Error" msgstr "Error" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Error uploading file" +msgstr "Error uploading file" + #: apps/remix/app/components/forms/team-document-preferences-form.tsx msgid "Everyone can access and view the document" msgstr "Everyone can access and view the document" @@ -2818,6 +2999,11 @@ msgstr "Expires on {0}" msgid "External ID" msgstr "External ID" +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +msgid "Failed to create folder" +msgstr "Failed to create folder" + #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx msgid "Failed to reseal document" msgstr "Failed to reseal document" @@ -2826,10 +3012,18 @@ msgstr "Failed to reseal document" msgid "Failed to save settings." msgstr "Failed to save settings." +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +msgid "Failed to update document" +msgstr "Failed to update document" + #: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx msgid "Failed to update recipient" msgstr "Failed to update recipient" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +msgid "Failed to update template" +msgstr "Failed to update template" + #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id.tsx #: apps/remix/app/routes/_authenticated+/settings+/webhooks.$id.tsx msgid "Failed to update webhook" @@ -2882,7 +3076,13 @@ msgstr "Field unsigned" msgid "Fields" msgstr "Fields" +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Fields updated" +msgstr "Fields updated" + #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx msgid "File cannot be larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB" msgstr "File cannot be larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB" @@ -2890,6 +3090,25 @@ msgstr "File cannot be larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB" msgid "File size exceeds the limit of {APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB" msgstr "File size exceeds the limit of {APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Folder" +msgstr "Folder" + +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +msgid "Folder created successfully" +msgstr "Folder created successfully" + +#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx +#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx +msgid "Folder not found" +msgstr "Folder not found" + +#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx +#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx +msgid "Folder updated successfully" +msgstr "Folder updated successfully" + #: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx #: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx #: packages/ui/primitives/document-flow/field-items-advanced-settings/name-field.tsx @@ -2936,6 +3155,7 @@ msgstr "Full Name" #: apps/remix/app/components/general/teams/team-settings-nav-desktop.tsx #: apps/remix/app/components/general/document/document-edit-form.tsx #: apps/remix/app/components/general/direct-template/direct-template-page.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx msgid "General" msgstr "General" @@ -2966,6 +3186,10 @@ msgstr "Go back home" msgid "Go Back Home" msgstr "Go Back Home" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "Go to document" +msgstr "Go to document" + #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx msgid "Go to owner" msgstr "Go to owner" @@ -3137,7 +3361,7 @@ msgstr "Invalid code. Please try again." msgid "Invalid email" msgstr "Invalid email" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx msgid "Invalid link" msgstr "Invalid link" @@ -3234,6 +3458,7 @@ msgid "Label" msgstr "Label" #: apps/remix/app/components/general/menu-switcher.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Language" @@ -3256,6 +3481,7 @@ msgstr "Last 7 days" msgid "Last modified" msgstr "Last modified" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Last updated" msgstr "Last updated" @@ -3358,6 +3584,7 @@ msgstr "Manage {0}'s profile" msgid "Manage all teams you are currently associated with." msgstr "Manage all teams you are currently associated with." +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Manage and view template" msgstr "Manage and view template" @@ -3466,6 +3693,7 @@ msgstr "Member Since" msgid "Members" msgstr "Members" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-subject.tsx msgid "Message <0>(Optional)" @@ -3493,19 +3721,38 @@ msgstr "Monthly Active Users: Users that created at least one Document" msgid "Monthly Active Users: Users that had at least one of their documents completed" msgstr "Monthly Active Users: Users that had at least one of their documents completed" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Move" msgstr "Move" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "Move \"{templateTitle}\" to a folder" +msgstr "Move \"{templateTitle}\" to a folder" + +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Move Document to Folder" +msgstr "Move Document to Folder" + #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Move Document to Team" msgstr "Move Document to Team" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "Move Template to Folder" +msgstr "Move Template to Folder" + #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "Move Template to Team" msgstr "Move Template to Team" +#: apps/remix/app/components/tables/templates-table-action-dropdown.tsx +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx +msgid "Move to Folder" +msgstr "Move to Folder" + #: apps/remix/app/components/tables/templates-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx msgid "Move to Team" @@ -3529,6 +3776,8 @@ msgstr "My templates" #: apps/remix/app/components/general/claim-account.tsx #: apps/remix/app/components/general/document-signing/document-signing-name-field.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx @@ -3617,8 +3866,8 @@ msgstr "No recent activity" msgid "No recent documents" msgstr "No recent documents" +#: packages/ui/primitives/recipient-selector.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx -#: packages/ui/primitives/document-flow/add-fields.tsx msgid "No recipient matching this description was found." msgstr "No recipient matching this description was found." @@ -3629,8 +3878,8 @@ msgstr "No recipient matching this description was found." msgid "No recipients" msgstr "No recipients" +#: packages/ui/primitives/recipient-selector.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx -#: packages/ui/primitives/document-flow/add-fields.tsx msgid "No recipients with this role" msgstr "No recipients with this role" @@ -3672,6 +3921,7 @@ msgstr "No value found." msgid "No worries, it happens! Enter your email and we'll email you a special link to reset your password." msgstr "No worries, it happens! Enter your email and we'll email you a special link to reset your password." +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/lib/constants/document.ts msgid "None" msgstr "None" @@ -3895,8 +4145,8 @@ msgstr "Payment overdue" #: apps/remix/app/components/tables/user-settings-teams-page-table.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx #: apps/remix/app/components/general/document/document-status.tsx -#: apps/remix/app/components/general/document/document-read-only-fields.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx +#: packages/ui/components/document/document-read-only-fields.tsx #: packages/lib/constants/document.ts msgid "Pending" msgstr "Pending" @@ -3984,6 +4234,11 @@ msgstr "Please check your email for updates." msgid "Please choose your new password" msgstr "Please choose your new password" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +msgid "Please configure the document first" +msgstr "Please configure the document first" + #: packages/lib/server-only/auth/send-confirmation-email.ts msgid "Please confirm your email" msgstr "Please confirm your email" @@ -4256,6 +4511,7 @@ msgstr "Recipient updated" #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx #: apps/remix/app/components/general/template/template-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx msgid "Recipients" msgstr "Recipients" @@ -4279,6 +4535,7 @@ msgstr "Recovery codes" msgid "Red" msgstr "Red" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Redirect URL" @@ -4340,7 +4597,6 @@ msgstr "Reminder: Please {recipientActionVerb} your document" #: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx #: apps/remix/app/components/general/teams/team-email-dropdown.tsx #: apps/remix/app/components/general/document-signing/document-signing-field-container.tsx -#: apps/remix/app/components/general/document-signing/document-signing-field-container.tsx #: apps/remix/app/components/forms/team-branding-preferences-form.tsx #: apps/remix/app/components/forms/avatar-image.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx @@ -4437,7 +4693,7 @@ msgstr "Retention of Documents" msgid "Retry" msgstr "Retry" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.decline.$token.tsx @@ -4468,6 +4724,7 @@ msgstr "Revoke access" #: apps/remix/app/components/tables/user-settings-current-teams-table.tsx #: apps/remix/app/components/tables/team-settings-members-table.tsx #: apps/remix/app/components/tables/team-settings-member-invites-table.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx #: apps/remix/app/components/dialogs/team-member-invite-dialog.tsx @@ -4479,12 +4736,19 @@ msgstr "Role" msgid "Roles" msgstr "Roles" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Root (No Folder)" +msgstr "Root (No Folder)" + #: packages/ui/primitives/data-table-pagination.tsx msgid "Rows per page" msgstr "Rows per page" #: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx #: apps/remix/app/components/general/document-signing/document-signing-number-field.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx msgid "Save" @@ -4539,6 +4803,10 @@ msgstr "Security activity" msgid "Select" msgstr "Select" +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Select a folder to move this document to." +msgstr "Select a folder to move this document to." + #: apps/remix/app/components/dialogs/template-move-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Select a team" @@ -4660,6 +4928,14 @@ msgstr "Sent" msgid "Set a password" msgstr "Set a password" +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Set up your document properties and recipient information" +msgstr "Set up your document properties and recipient information" + +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Set up your template properties and recipient information" +msgstr "Set up your template properties and recipient information" + #: apps/remix/app/routes/_authenticated+/settings+/_layout.tsx #: apps/remix/app/components/general/app-nav-mobile.tsx #: apps/remix/app/components/general/app-command-menu.tsx @@ -4801,7 +5077,6 @@ msgstr "Sign Up with OIDC" #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/types.ts -#: packages/ui/primitives/document-flow/field-icon.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Signature" msgstr "Signature" @@ -4831,8 +5106,8 @@ msgid "Signatures will appear once the document has been completed" msgstr "Signatures will appear once the document has been completed" #: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx -#: apps/remix/app/components/general/document/document-read-only-fields.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx +#: packages/ui/components/document/document-read-only-fields.tsx #: packages/lib/constants/recipient-roles.ts msgid "Signed" msgstr "Signed" @@ -4929,7 +5204,9 @@ msgstr "Some signers have not been assigned a signature field. Please assign at #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx +#: apps/remix/app/components/general/share-document-download-button.tsx #: apps/remix/app/components/general/billing-portal-button.tsx #: apps/remix/app/components/general/billing-plans.tsx #: apps/remix/app/components/general/teams/team-layout-billing-banner.tsx @@ -4941,6 +5218,7 @@ msgstr "Some signers have not been assigned a signature field. Please assign at #: apps/remix/app/components/general/document-signing/document-signing-auth-page.tsx #: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx #: apps/remix/app/components/general/document/document-page-view-dropdown.tsx +#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx #: apps/remix/app/components/general/document/document-certificate-download-button.tsx #: apps/remix/app/components/general/document/document-audit-log-download-button.tsx @@ -4967,7 +5245,7 @@ msgid "Something went wrong" msgstr "Something went wrong" #. placeholder {0}: data.teamName -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Something went wrong while attempting to transfer the ownership of team <0>{0} to your. Please try again later or contact support." msgstr "Something went wrong while attempting to transfer the ownership of team <0>{0} to your. Please try again later or contact support." @@ -5039,6 +5317,7 @@ msgstr "Status" msgid "Step <0>{step} of {maxStep}" msgstr "Step <0>{step} of {maxStep}" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-subject.tsx msgid "Subject <0>(Optional)" @@ -5056,6 +5335,8 @@ msgstr "Subscription" msgid "Subscriptions" msgstr "Subscriptions" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx #: apps/remix/app/components/tables/user-settings-pending-teams-table-actions.tsx #: apps/remix/app/components/tables/team-settings-member-invites-table.tsx @@ -5196,15 +5477,15 @@ msgstr "Team Only" msgid "Team only templates are not linked anywhere and are visible only to your team." msgstr "Team only templates are not linked anywhere and are visible only to your team." -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Team ownership transfer" msgstr "Team ownership transfer" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Team ownership transfer already completed!" msgstr "Team ownership transfer already completed!" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Team ownership transferred!" msgstr "Team ownership transferred!" @@ -5253,6 +5534,8 @@ msgstr "Teams" msgid "Teams restricted" msgstr "Teams restricted" +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id.edit.tsx +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id.edit.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx @@ -5263,6 +5546,10 @@ msgstr "Teams restricted" msgid "Template" msgstr "Template" +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Template Created" +msgstr "Template Created" + #: apps/remix/app/components/dialogs/template-delete-dialog.tsx msgid "Template deleted" msgstr "Template deleted" @@ -5283,6 +5570,11 @@ msgstr "Template has been removed from your public profile." msgid "Template has been updated." msgstr "Template has been updated." +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Template is using legacy field insertion" +msgstr "Template is using legacy field insertion" + +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "Template moved" msgstr "Template moved" @@ -5299,6 +5591,12 @@ msgstr "Template saved" msgid "Template title" msgstr "Template title" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +msgid "Template updated successfully" +msgstr "Template updated successfully" + +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx #: apps/remix/app/components/general/app-nav-mobile.tsx @@ -5374,10 +5672,18 @@ msgstr "The content to show in the banner, HTML is allowed" msgid "The direct link has been copied to your clipboard" msgstr "The direct link has been copied to your clipboard" +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "The document has been moved successfully." +msgstr "The document has been moved successfully." + #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "The document has been successfully moved to the selected team." msgstr "The document has been successfully moved to the selected team." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "The document is already saved and cannot be changed." +msgstr "The document is already saved and cannot be changed." + #: apps/remix/app/components/embed/embed-document-completed.tsx msgid "The document is now completed, please follow any instructions provided within the parent application." msgstr "The document is now completed, please follow any instructions provided within the parent application." @@ -5416,6 +5722,28 @@ msgstr "The email or password provided is incorrect" msgid "The events that will trigger a webhook to be sent to your URL." msgstr "The events that will trigger a webhook to be sent to your URL." +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "The fields have been updated to the new field insertion method successfully" +msgstr "The fields have been updated to the new field insertion method successfully" + +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx +msgid "The folder you are trying to delete does not exist." +msgstr "The folder you are trying to delete does not exist." + +#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx +#: apps/remix/app/components/dialogs/folder-move-dialog.tsx +msgid "The folder you are trying to move does not exist." +msgstr "The folder you are trying to move does not exist." + +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "The folder you are trying to move the document to does not exist." +msgstr "The folder you are trying to move the document to does not exist." + +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "The folder you are trying to move the template to does not exist." +msgstr "The folder you are trying to move the template to does not exist." + #: packages/email/templates/bulk-send-complete.tsx msgid "The following errors occurred:" msgstr "The following errors occurred:" @@ -5429,7 +5757,7 @@ msgid "The following team has been deleted by you" msgstr "The following team has been deleted by you" #. placeholder {0}: data.teamName -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "The ownership of team <0>{0} has been successfully transferred to you." msgstr "The ownership of team <0>{0} has been successfully transferred to you." @@ -5533,6 +5861,10 @@ msgstr "" "The team you are looking for may have been removed, renamed or may have never\n" " existed." +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "The template has been moved successfully." +msgstr "The template has been moved successfully." + #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "The template has been successfully moved to the selected team." msgstr "The template has been successfully moved to the selected team." @@ -5588,6 +5920,10 @@ msgstr "There are no active drafts at the current moment. You can upload a docum msgid "There are no completed documents yet. Documents that you have created or received will appear here once completed." msgstr "There are no completed documents yet. Documents that you have created or received will appear here once completed." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "There was an error uploading your file. Please try again." +msgstr "There was an error uploading your file. Please try again." + #: apps/remix/app/components/general/teams/team-email-usage.tsx msgid "They have permission on your behalf to:" msgstr "They have permission on your behalf to:" @@ -5618,6 +5954,10 @@ msgstr "This can be overriden by setting the authentication requirements directl msgid "This document can not be recovered, if you would like to dispute the reason for future documents please contact support." msgstr "This document can not be recovered, if you would like to dispute the reason for future documents please contact support." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "This document cannot be changed" +msgstr "This document cannot be changed" + #: apps/remix/app/components/dialogs/document-delete-dialog.tsx msgid "This document could not be deleted at this time. Please try again." msgstr "This document could not be deleted at this time. Please try again." @@ -5630,7 +5970,7 @@ msgstr "This document could not be duplicated at this time. Please try again." msgid "This document could not be re-sent at this time. Please try again." msgstr "This document could not be re-sent at this time. Please try again." -#: packages/ui/primitives/document-flow/add-fields.tsx +#: packages/ui/primitives/recipient-selector.tsx msgid "This document has already been sent to this recipient. You can no longer edit this recipient." msgstr "This document has already been sent to this recipient. You can no longer edit this recipient." @@ -5642,18 +5982,29 @@ msgstr "This document has been cancelled by the owner and is no longer available msgid "This document has been cancelled by the owner." msgstr "This document has been cancelled by the owner." +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "This document has been rejected by a recipient" msgstr "This document has been rejected by a recipient" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "This document has been signed by all recipients" msgstr "This document has been signed by all recipients" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "This document is available in your Documenso account. You can view more details, recipients, and audit logs there." +msgstr "This document is available in your Documenso account. You can view more details, recipients, and audit logs there." + +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "This document is currently a draft and has not been sent" msgstr "This document is currently a draft and has not been sent" +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "This document is using legacy field insertion, we recommend using the new field insertion method for more accurate results." +msgstr "This document is using legacy field insertion, we recommend using the new field insertion method for more accurate results." + #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx msgid "This document was created by you or a team member using the template above." msgstr "This document was created by you or a team member using the template above." @@ -5694,11 +6045,16 @@ msgstr "This email will be sent to the recipient who has just signed the documen msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them." msgstr "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them." +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +#: apps/remix/app/components/dialogs/folder-create-dialog.tsx +msgid "This folder name is already taken." +msgstr "This folder name is already taken." + #: packages/ui/primitives/template-flow/add-template-settings.tsx msgid "This is how the document will reach the recipients once the document is ready for signing." msgstr "This is how the document will reach the recipients once the document is ready for signing." -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "This link is invalid or has expired. Please contact your team to resend a transfer request." msgstr "This link is invalid or has expired. Please contact your team to resend a transfer request." @@ -5738,6 +6094,10 @@ msgstr "This team, and any associated data excluding billing invoices will be pe msgid "This template could not be deleted at this time. Please try again." msgstr "This template could not be deleted at this time. Please try again." +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "This template is using legacy field insertion, we recommend using the new field insertion method for more accurate results." +msgstr "This template is using legacy field insertion, we recommend using the new field insertion method for more accurate results." + #: apps/remix/app/routes/_unauthenticated+/team.decline.$token.tsx msgid "This token is invalid or has expired. No action is needed." msgstr "This token is invalid or has expired. No action is needed." @@ -5776,11 +6136,13 @@ msgstr "This will override any global settings." msgid "Time" msgstr "Time" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Time zone" msgstr "Time zone" #: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Time Zone" @@ -5790,6 +6152,7 @@ msgstr "Time Zone" #: apps/remix/app/components/tables/templates-table.tsx #: apps/remix/app/components/tables/documents-table.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Title" msgstr "Title" @@ -6099,6 +6462,10 @@ msgstr "Update" msgid "Update Banner" msgstr "Update Banner" +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Update Fields" +msgstr "Update Fields" + #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx msgid "Update passkey" msgstr "Update passkey" @@ -6113,6 +6480,7 @@ msgstr "Update profile" #: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx +#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx msgid "Update Recipient" msgstr "Update Recipient" @@ -6155,10 +6523,15 @@ msgstr "Updating password..." msgid "Updating Your Information" msgstr "Updating Your Information" +#: packages/ui/primitives/document-upload.tsx #: packages/ui/primitives/document-dropzone.tsx msgid "Upgrade" msgstr "Upgrade" +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +msgid "Upgrade your plan to upload more documents" +msgstr "Upgrade your plan to upload more documents" + #: packages/lib/constants/document.ts msgid "Upload" msgstr "Upload" @@ -6187,10 +6560,17 @@ msgstr "Upload CSV" msgid "Upload custom document" msgstr "Upload custom document" +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +#: packages/ui/primitives/document-upload.tsx +msgid "Upload Document" +msgstr "Upload Document" + #: packages/ui/primitives/signature-pad/signature-pad-upload.tsx msgid "Upload Signature" msgstr "Upload Signature" +#: packages/ui/primitives/document-upload.tsx #: packages/ui/primitives/document-dropzone.tsx msgid "Upload Template Document" msgstr "Upload Template Document" @@ -6216,6 +6596,11 @@ msgstr "Uploaded file is too small" msgid "Uploaded file not an allowed file type" msgstr "Uploaded file not an allowed file type" +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +msgid "Uploading document..." +msgstr "Uploading document..." + +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Use" msgstr "Use" @@ -6664,6 +7049,7 @@ msgstr "We will generate signing links for you, which you can send to the recipi msgid "We won't send anything to notify recipients." msgstr "We won't send anything to notify recipients." +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx #: apps/remix/app/routes/_authenticated+/templates._index.tsx #: apps/remix/app/components/tables/documents-table-empty-state.tsx msgid "We're all empty" @@ -6728,6 +7114,7 @@ msgstr "Welcome to Documenso!" msgid "Were you trying to edit this document instead?" msgstr "Were you trying to edit this document instead?" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "When enabled, signers can choose who should sign next in the sequence instead of following the predefined order." @@ -6887,11 +7274,13 @@ msgstr "You cannot have more than {MAXIMUM_PASSKEYS} passkeys." msgid "You cannot modify a team member who has a higher role than you." msgstr "You cannot modify a team member who has a higher role than you." +#: packages/ui/primitives/document-upload.tsx #: packages/ui/primitives/document-dropzone.tsx msgid "You cannot upload documents at this time." msgstr "You cannot upload documents at this time." #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "You cannot upload encrypted PDFs" msgstr "You cannot upload encrypted PDFs" @@ -6913,7 +7302,7 @@ msgid "You have accepted an invitation from <0>{0} to join their team." msgstr "You have accepted an invitation from <0>{0} to join their team." #. placeholder {0}: data.teamName -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "You have already completed the ownership transfer for <0>{0}." msgstr "You have already completed the ownership transfer for <0>{0}." @@ -6963,6 +7352,7 @@ msgstr "You have initiated the document {0} that requires you to {recipientActio msgid "You have no webhooks yet. Your webhooks will be shown here once you create them." msgstr "You have no webhooks yet. Your webhooks will be shown here once you create them." +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx #: apps/remix/app/routes/_authenticated+/templates._index.tsx msgid "You have not yet created any templates. To create a template please upload one." msgstr "You have not yet created any templates. To create a template please upload one." @@ -6977,6 +7367,7 @@ msgid "You have reached the maximum limit of {0} direct templates. <0>Upgrade yo msgstr "You have reached the maximum limit of {0} direct templates. <0>Upgrade your account to continue!" #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "You have reached your document limit for this month. Please upgrade your plan." msgstr "You have reached your document limit for this month. Please upgrade your plan." @@ -7050,6 +7441,11 @@ msgstr "You must have at least one other team member to transfer ownership." msgid "You must set a profile URL before enabling your public profile." msgstr "You must set a profile URL before enabling your public profile." +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx +msgid "You must type '{deleteMessage}' to confirm" +msgstr "You must type '{deleteMessage}' to confirm" + #: apps/remix/app/routes/_authenticated+/settings+/tokens.tsx msgid "You need to be an admin to manage API tokens." msgstr "You need to be an admin to manage API tokens." @@ -7115,6 +7511,8 @@ msgid "Your direct signing templates" msgstr "Your direct signing templates" #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx msgid "Your document failed to upload." msgstr "Your document failed to upload." @@ -7122,6 +7520,10 @@ msgstr "Your document failed to upload." msgid "Your document has been created from the template successfully." msgstr "Your document has been created from the template successfully." +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Your document has been created successfully" +msgstr "Your document has been created successfully" + #: packages/email/template-components/template-document-super-delete.tsx msgid "Your document has been deleted by an admin!" msgstr "Your document has been deleted by an admin!" @@ -7139,6 +7541,7 @@ msgid "Your document has been successfully duplicated." msgstr "Your document has been successfully duplicated." #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "Your document has been uploaded successfully." msgstr "Your document has been uploaded successfully." @@ -7232,6 +7635,10 @@ msgstr "Your team has been successfully deleted." msgid "Your team has been successfully updated." msgstr "Your team has been successfully updated." +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Your template has been created successfully" +msgstr "Your template has been created successfully" + #: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx msgid "Your template has been duplicated successfully." msgstr "Your template has been duplicated successfully." diff --git a/packages/lib/translations/es/web.po b/packages/lib/translations/es/web.po index 01b985db5..0c228a851 100644 --- a/packages/lib/translations/es/web.po +++ b/packages/lib/translations/es/web.po @@ -26,6 +26,10 @@ msgstr " Habilitar la firma mediante enlace directo" msgid " The events that will trigger a webhook to be sent to your URL." msgstr " Los eventos que activarán un webhook para ser enviado a tu URL." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid ".PDF documents accepted (max {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB)" +msgstr "" + #. placeholder {0}: team.name #: apps/remix/app/components/forms/team-document-preferences-form.tsx msgid "\"{0}\" has invited you to sign \"example document\"." @@ -96,11 +100,13 @@ msgid "{0, plural, one {1 matching field} other {# matching fields}}" msgstr "{0, plural, one {1 campo que coincide} other {# campos que coinciden}}" #. placeholder {0}: recipients.length +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.edit.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.edit.tsx msgid "{0, plural, one {1 Recipient} other {# Recipients}}" msgstr "{0, plural, one {1 Destinatario} other {# Destinatarios}}" #. placeholder {0}: pendingRecipients.length +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}" msgstr "{0, plural, one {Esperando 1 destinatario} other {Esperando # destinatarios}}" @@ -144,6 +150,7 @@ msgstr "{0} dejó el equipo {teamName} en Documenso" #. placeholder {0}: remaining.documents #. placeholder {1}: quota.documents #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "{0} of {1} documents remaining this month." msgstr "{0} de {1} documentos restantes este mes." @@ -161,6 +168,7 @@ msgid "{0} on behalf of \"{1}\" has invited you to {recipientActionVerb} the doc msgstr "{0} en nombre de \"{1}\" te ha invitado a {recipientActionVerb} el documento \"{2}\"." #. placeholder {0}: recipients.length +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "{0} Recipient(s)" msgstr "{0} Destinatario(s)" @@ -327,6 +335,10 @@ msgstr "{recipientActionVerb} documento" msgid "{recipientActionVerb} the document to complete the process." msgstr "{recipientActionVerb} el documento para completar el proceso." +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "{recipientCount} recipients" +msgstr "" + #: packages/email/templates/document-created-from-direct-template.tsx msgid "{recipientName} {action} a document by using one of your direct links" msgstr "{recipientName} {action} un documento utilizando uno de tus enlaces directos" @@ -727,6 +739,7 @@ msgstr "Agregar" msgid "Add a document" msgstr "Agregar un documento" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Add a URL to redirect the user to once the document is signed" @@ -800,6 +813,7 @@ msgstr "Agregar destinatario de marcador de posición" msgid "Add Placeholders" msgstr "Agregar Marcadores de posición" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Add Signer" msgstr "Agregar firmante" @@ -808,6 +822,10 @@ msgstr "Agregar firmante" msgid "Add Signers" msgstr "Agregar Firmantes" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +msgid "Add signers and configure signing preferences" +msgstr "" + #: apps/remix/app/components/dialogs/team-email-add-dialog.tsx msgid "Add team email" msgstr "Agregar correo electrónico del equipo" @@ -853,11 +871,16 @@ msgstr "Panel administrativo" msgid "Advanced Options" msgstr "Opciones avanzadas" +#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Advanced settings" msgstr "Configuraciones avanzadas" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Advanced Settings" +msgstr "" + #: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx msgid "After signing a document electronically, you will be provided the opportunity to view, download, and print the document for your records. It is highly recommended that you retain a copy of all electronically signed documents for your personal records. We will also retain a copy of the signed document for our records however we may not be able to provide you with a copy of the signed document after a certain period of time." msgstr "Después de firmar un documento electrónicamente, se le dará la oportunidad de ver, descargar e imprimir el documento para sus registros. Se recomienda encarecidamente que conserve una copia de todos los documentos firmados electrónicamente para sus registros personales. También mantendremos una copia del documento firmado para nuestros registros, sin embargo, es posible que no podamos proporcionarle una copia del documento firmado después de un cierto período de tiempo." @@ -882,6 +905,11 @@ msgstr "Todos los documentos han sido procesados. Cualquier nuevo documento que msgid "All documents related to the electronic signing process will be provided to you electronically through our platform or via email. It is your responsibility to ensure that your email address is current and that you can receive and open our emails." msgstr "Todos los documentos relacionados con el proceso de firma electrónica se le proporcionarán electrónicamente a través de nuestra plataforma o por correo electrónico. Es su responsabilidad asegurarse de que su dirección de correo electrónico esté actualizada y que pueda recibir y abrir nuestros correos electrónicos." +#: apps/remix/app/routes/_authenticated+/templates.folders._index.tsx +#: apps/remix/app/routes/_authenticated+/documents.folders._index.tsx +msgid "All Folders" +msgstr "" + #: apps/remix/app/components/dialogs/document-delete-dialog.tsx msgid "All inserted signatures will be voided" msgstr "Todas las firmas insertadas serán anuladas" @@ -910,11 +938,13 @@ msgstr "Todo el Tiempo" msgid "Allow document recipients to reply directly to this email address" msgstr "Permitir que los destinatarios del documento respondan directamente a esta dirección de correo electrónico" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Allow signers to dictate next signer" msgstr "Permitir a los firmantes dictar al siguiente firmante" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Allowed Signature Types" @@ -998,8 +1028,11 @@ msgstr "Ocurrió un error al desactivar la firma de enlace directo." msgid "An error occurred while disabling the user." msgstr "Se produjo un error al deshabilitar al usuario." +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx +#: apps/remix/app/components/general/share-document-download-button.tsx +#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx msgid "An error occurred while downloading your document." @@ -1025,10 +1058,12 @@ msgstr "Ocurrió un error al cargar los miembros del equipo. Por favor intenta d msgid "An error occurred while loading the document." msgstr "Se produjo un error al cargar el documento." +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "An error occurred while moving the document." msgstr "Ocurrió un error al mover el documento." +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "An error occurred while moving the template." msgstr "Ocurrió un error al mover la plantilla." @@ -1103,6 +1138,7 @@ msgid "An error occurred while updating your profile." msgstr "Ocurrió un error al actualizar tu perfil." #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "An error occurred while uploading your document." msgstr "Ocurrió un error al subir tu documento." @@ -1140,6 +1176,21 @@ msgstr "Ocurrió un error inesperado." msgid "An unknown error occurred" msgstr "Ocurrió un error desconocido" +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +#: apps/remix/app/components/dialogs/folder-create-dialog.tsx +msgid "An unknown error occurred while creating the folder." +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx +msgid "An unknown error occurred while deleting the folder." +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx +#: apps/remix/app/components/dialogs/folder-move-dialog.tsx +msgid "An unknown error occurred while moving the folder." +msgstr "" + #: apps/remix/app/components/dialogs/team-transfer-dialog.tsx msgid "Any payment methods attached to this team will remain attached to this team. Please contact us if you need to update this information." msgstr "Cualquier método de pago adjunto a este equipo permanecerá adjunto a este equipo. Por favor, contáctanos si necesitas actualizar esta información." @@ -1297,6 +1348,8 @@ msgstr "Esperando confirmación de correo electrónico" #: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx #: apps/remix/app/components/forms/signup.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx msgid "Back" msgstr "Atrás" @@ -1447,6 +1500,7 @@ msgstr "Puede preparar" #: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx #: apps/remix/app/components/dialogs/webhook-create-dialog.tsx #: apps/remix/app/components/dialogs/token-delete-dialog.tsx +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx #: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx @@ -1466,6 +1520,7 @@ msgstr "Puede preparar" #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx #: apps/remix/app/components/dialogs/passkey-create-dialog.tsx #: apps/remix/app/components/dialogs/document-resend-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx #: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx #: apps/remix/app/components/dialogs/document-delete-dialog.tsx @@ -1480,6 +1535,10 @@ msgstr "Cancelar" msgid "Cancelled by user" msgstr "Cancelado por el usuario" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Cannot remove document" +msgstr "" + #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Cannot remove signer" msgstr "No se puede eliminar el firmante" @@ -1533,6 +1592,10 @@ msgstr "Elija el destinatario del enlace directo" msgid "Choose how the document will reach recipients" msgstr "Elige cómo el documento llegará a los destinatarios" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Choose how to distribute your document to recipients. Email will send notifications, None will generate signing links for manual distribution." +msgstr "" + #: apps/remix/app/components/forms/token.tsx msgid "Choose..." msgstr "Elija..." @@ -1602,6 +1665,10 @@ msgstr "Haga clic para insertar campo" msgid "Close" msgstr "Cerrar" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Communication" +msgstr "" + #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/forms/signup.tsx @@ -1658,10 +1725,33 @@ msgstr "Documentos completados" msgid "Completed Documents" msgstr "Documentos Completados" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "Completed on {formattedDate}" +msgstr "" + +#. placeholder {0}: parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[currentField.type]) +#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx +msgid "Configure {0} Field" +msgstr "" + +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Configure additional options and preferences" +msgstr "" + #: packages/lib/constants/template.ts msgid "Configure Direct Recipient" msgstr "Configurar destinatario directo" +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Configure Document" +msgstr "" + +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +msgid "Configure Fields" +msgstr "" + #: apps/remix/app/components/general/document/document-edit-form.tsx msgid "Configure general settings for the document." msgstr "Configurar ajustes generales para el documento." @@ -1674,12 +1764,22 @@ msgstr "Configurar ajustes generales para la plantilla." msgid "Configure template" msgstr "Configurar plantilla" +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Configure Template" +msgstr "" + #. placeholder {0}: parseMessageDescriptor( _, FRIENDLY_FIELD_TYPE[currentField.type], ) +#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Configure the {0} field" msgstr "Configurar el campo {0}" +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +msgid "Configure the fields you want to place on the document." +msgstr "" + #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx msgid "Confirm" @@ -1695,6 +1795,8 @@ msgstr "Confirme escribiendo <0>{deleteMessage}" #: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx #: apps/remix/app/components/dialogs/token-delete-dialog.tsx +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx msgid "Confirm by typing: <0>{deleteMessage}" msgstr "Confirme escribiendo: <0>{deleteMessage}" @@ -1724,12 +1826,13 @@ msgid "Content" msgstr "Contenido" #: apps/remix/app/routes/_unauthenticated+/verify-email.$token.tsx -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx #: apps/remix/app/components/general/document-signing/document-signing-form.tsx +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx #: apps/remix/app/components/dialogs/passkey-create-dialog.tsx #: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx @@ -1940,6 +2043,7 @@ msgstr "Creado" msgid "Created At" msgstr "Creado En" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Created by" msgstr "Creado por" @@ -1989,10 +2093,12 @@ msgstr "Modo Oscuro" msgid "Date" msgstr "Fecha" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Date created" msgstr "Fecha de creación" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Date Format" @@ -2042,8 +2148,11 @@ msgstr "Eliminar" #. placeholder {0}: webhook.webhookUrl #. placeholder {0}: token.name +#. placeholder {0}: folder?.name ?? 'folder' #: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx #: apps/remix/app/components/dialogs/token-delete-dialog.tsx +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx msgid "delete {0}" msgstr "eliminar {0}" @@ -2217,6 +2326,10 @@ msgstr "Mostrar su nombre y correo electrónico en documentos" msgid "Distribute Document" msgstr "Distribuir documento" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Distribution Method" +msgstr "" + #: apps/remix/app/components/dialogs/template-delete-dialog.tsx msgid "Do you want to delete this template?" msgstr "¿Desea eliminar esta plantilla?" @@ -2229,6 +2342,7 @@ msgstr "¿Desea duplicar esta plantilla?" msgid "Documenso will delete <0>all of your documents, along with all of your completed documents, signatures, and all other resources belonging to your Account." msgstr "Documenso eliminará <0>todos sus documentos, junto con todos sus documentos completados, firmas y todos los demás recursos de su cuenta." +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx msgid "Document" @@ -2294,6 +2408,10 @@ msgstr "¡Documento completado!" msgid "Document created" msgstr "Documento creado" +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Document Created" +msgstr "" + #. placeholder {0}: document.user.name #: apps/remix/app/components/general/template/template-page-view-recent-activity.tsx msgid "Document created by <0>{0}" @@ -2312,6 +2430,7 @@ msgstr "Documento creado usando un <0>enlace directo" msgid "Document Creation" msgstr "Creación de documento" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx #: apps/remix/app/components/dialogs/document-delete-dialog.tsx #: apps/remix/app/components/dialogs/admin-document-delete-dialog.tsx @@ -2344,12 +2463,18 @@ msgstr "Documento duplicado" msgid "Document external ID updated" msgstr "ID externo del documento actualizado" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "Document found in your account" +msgstr "" + +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx #: apps/remix/app/components/general/document/document-history-sheet.tsx msgid "Document history" msgstr "Historial de documentos" #: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Document ID" msgstr "ID del documento" @@ -2358,6 +2483,14 @@ msgstr "ID del documento" msgid "Document inbox" msgstr "Bandeja de documentos" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Document is already uploaded" +msgstr "" + +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Document is using legacy field insertion" +msgstr "" + #: apps/remix/app/components/tables/templates-table.tsx msgid "Document Limit Exceeded!" msgstr "¡Límite de documentos excedido!" @@ -2366,6 +2499,7 @@ msgstr "¡Límite de documentos excedido!" msgid "Document metrics" msgstr "Métricas de documento" +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Document moved" msgstr "Documento movido" @@ -2429,10 +2563,12 @@ msgstr "Se actualizó la autenticación de firma del documento" msgid "Document signing process will be cancelled" msgstr "El proceso de firma del documento será cancelado" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Document status" msgstr "Estado del documento" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Document title" msgstr "Título del documento" @@ -2445,11 +2581,16 @@ msgstr "Título del documento actualizado" msgid "Document updated" msgstr "Documento actualizado" +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +msgid "Document updated successfully" +msgstr "" + #: apps/remix/app/components/general/document/document-upload.tsx msgid "Document upload disabled due to unpaid invoices" msgstr "La carga de documentos está deshabilitada debido a facturas impagadas" #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "Document uploaded" msgstr "Documento subido" @@ -2466,6 +2607,9 @@ msgid "Document will be permanently deleted" msgstr "El documento será eliminado permanentemente" #: apps/remix/app/routes/_profile+/p.$url.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId._index.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.edit.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.edit.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx @@ -2480,6 +2624,7 @@ msgstr "El documento será eliminado permanentemente" msgid "Documents" msgstr "Documentos" +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Documents created from template" msgstr "Documentos creados a partir de la plantilla" @@ -2500,6 +2645,7 @@ msgstr "¿No tienes una cuenta? <0>Regístrate" #: apps/remix/app/components/tables/team-settings-billing-invoices-table.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx +#: apps/remix/app/components/general/share-document-download-button.tsx #: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx #: apps/remix/app/components/forms/2fa/view-recovery-codes-dialog.tsx @@ -2517,6 +2663,11 @@ msgstr "Descargar registros de auditoría" msgid "Download Certificate" msgstr "Descargar certificado" +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx +#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx +msgid "Download Original" +msgstr "" + #: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx msgid "Download Template CSV" msgstr "Descargar Plantilla CSV" @@ -2539,10 +2690,22 @@ msgstr "Documentos redactados" msgid "Drag & drop your PDF here." msgstr "Arrastre y suelte su PDF aquí." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Drag and drop or click to upload" +msgstr "" + +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +msgid "Drag and drop your PDF file here" +msgstr "" + #: packages/lib/constants/document.ts msgid "Draw" msgstr "Dibujar" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Drop your document here" +msgstr "" + #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Dropdown" @@ -2577,6 +2740,7 @@ msgstr "Duplicar" msgid "Edit" msgstr "Editar" +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Edit Template" msgstr "Editar plantilla" @@ -2607,6 +2771,9 @@ msgstr "Divulgación de Firma Electrónica" #: apps/remix/app/components/forms/forgot-password.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx @@ -2700,6 +2867,7 @@ msgstr "Habilitar branding personalizado para todos los documentos en este equip msgid "Enable Direct Link Signing" msgstr "Habilitar firma de enlace directo" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Enable signing order" @@ -2746,6 +2914,10 @@ msgstr "Ingresa tu nombre" msgid "Enter your text here" msgstr "Ingresa tu texto aquí" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx #: apps/remix/app/components/general/verify-email-banner.tsx @@ -2780,10 +2952,15 @@ msgstr "Ingresa tu texto aquí" #: apps/remix/app/components/general/document/document-edit-form.tsx #: apps/remix/app/components/general/document/document-edit-form.tsx #: apps/remix/app/components/general/document/document-edit-form.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx #: apps/remix/app/components/dialogs/webhook-create-dialog.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx #: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx #: apps/remix/app/components/dialogs/admin-user-enable-dialog.tsx #: apps/remix/app/components/dialogs/admin-user-disable-dialog.tsx @@ -2793,6 +2970,10 @@ msgstr "Ingresa tu texto aquí" msgid "Error" msgstr "Error" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Error uploading file" +msgstr "" + #: apps/remix/app/components/forms/team-document-preferences-form.tsx msgid "Everyone can access and view the document" msgstr "Todos pueden acceder y ver el documento" @@ -2823,6 +3004,11 @@ msgstr "Expira el {0}" msgid "External ID" msgstr "ID externo" +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +msgid "Failed to create folder" +msgstr "" + #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx msgid "Failed to reseal document" msgstr "Falló al volver a sellar el documento" @@ -2831,10 +3017,18 @@ msgstr "Falló al volver a sellar el documento" msgid "Failed to save settings." msgstr "Fallo al guardar configuraciones." +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +msgid "Failed to update document" +msgstr "" + #: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx msgid "Failed to update recipient" msgstr "Falló al actualizar el destinatario" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +msgid "Failed to update template" +msgstr "" + #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id.tsx #: apps/remix/app/routes/_authenticated+/settings+/webhooks.$id.tsx msgid "Failed to update webhook" @@ -2887,7 +3081,13 @@ msgstr "Campo no firmado" msgid "Fields" msgstr "Campos" +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Fields updated" +msgstr "" + #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx msgid "File cannot be larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB" msgstr "El archivo no puede ser mayor a {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB" @@ -2895,6 +3095,25 @@ msgstr "El archivo no puede ser mayor a {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB" msgid "File size exceeds the limit of {APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB" msgstr "El tamaño del archivo excede el límite de {APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Folder" +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +msgid "Folder created successfully" +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx +#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx +msgid "Folder not found" +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx +#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx +msgid "Folder updated successfully" +msgstr "" + #: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx #: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx #: packages/ui/primitives/document-flow/field-items-advanced-settings/name-field.tsx @@ -2941,6 +3160,7 @@ msgstr "Nombre completo" #: apps/remix/app/components/general/teams/team-settings-nav-desktop.tsx #: apps/remix/app/components/general/document/document-edit-form.tsx #: apps/remix/app/components/general/direct-template/direct-template-page.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx msgid "General" msgstr "General" @@ -2971,6 +3191,10 @@ msgstr "Regresar a casa" msgid "Go Back Home" msgstr "Regresar a casa" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "Go to document" +msgstr "" + #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx msgid "Go to owner" msgstr "Ir al propietario" @@ -3142,7 +3366,7 @@ msgstr "Código inválido. Por favor, intenta nuevamente." msgid "Invalid email" msgstr "Email inválido" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx msgid "Invalid link" msgstr "Enlace inválido" @@ -3239,6 +3463,7 @@ msgid "Label" msgstr "Etiqueta" #: apps/remix/app/components/general/menu-switcher.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Language" @@ -3261,6 +3486,7 @@ msgstr "Últimos 7 días" msgid "Last modified" msgstr "Última modificación" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Last updated" msgstr "Última actualización" @@ -3363,6 +3589,7 @@ msgstr "Gestionar el perfil de {0}" msgid "Manage all teams you are currently associated with." msgstr "Gestionar todos los equipos con los que estás asociado actualmente." +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Manage and view template" msgstr "Gestionar y ver plantilla" @@ -3471,6 +3698,7 @@ msgstr "Miembro desde" msgid "Members" msgstr "Miembros" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-subject.tsx msgid "Message <0>(Optional)" @@ -3498,19 +3726,38 @@ msgstr "Usuarios activos mensuales: Usuarios que crearon al menos un documento" msgid "Monthly Active Users: Users that had at least one of their documents completed" msgstr "Usuarios activos mensuales: Usuarios que completaron al menos uno de sus documentos" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Move" msgstr "Mover" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "Move \"{templateTitle}\" to a folder" +msgstr "" + +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Move Document to Folder" +msgstr "" + #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Move Document to Team" msgstr "Mover documento al equipo" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "Move Template to Folder" +msgstr "" + #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "Move Template to Team" msgstr "Mover plantilla al equipo" +#: apps/remix/app/components/tables/templates-table-action-dropdown.tsx +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx +msgid "Move to Folder" +msgstr "" + #: apps/remix/app/components/tables/templates-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx msgid "Move to Team" @@ -3534,6 +3781,8 @@ msgstr "Mis plantillas" #: apps/remix/app/components/general/claim-account.tsx #: apps/remix/app/components/general/document-signing/document-signing-name-field.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx @@ -3622,8 +3871,8 @@ msgstr "No hay actividad reciente" msgid "No recent documents" msgstr "No hay documentos recientes" +#: packages/ui/primitives/recipient-selector.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx -#: packages/ui/primitives/document-flow/add-fields.tsx msgid "No recipient matching this description was found." msgstr "No se encontró ningún destinatario que coincidiera con esta descripción." @@ -3634,8 +3883,8 @@ msgstr "No se encontró ningún destinatario que coincidiera con esta descripci msgid "No recipients" msgstr "Sin destinatarios" +#: packages/ui/primitives/recipient-selector.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx -#: packages/ui/primitives/document-flow/add-fields.tsx msgid "No recipients with this role" msgstr "No hay destinatarios con este rol" @@ -3677,6 +3926,7 @@ msgstr "No se encontró valor." msgid "No worries, it happens! Enter your email and we'll email you a special link to reset your password." msgstr "¡No te preocupes, sucede! Ingresa tu correo electrónico y te enviaremos un enlace especial para restablecer tu contraseña." +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/lib/constants/document.ts msgid "None" msgstr "Ninguno" @@ -3900,8 +4150,8 @@ msgstr "Pago atrasado" #: apps/remix/app/components/tables/user-settings-teams-page-table.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx #: apps/remix/app/components/general/document/document-status.tsx -#: apps/remix/app/components/general/document/document-read-only-fields.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx +#: packages/ui/components/document/document-read-only-fields.tsx #: packages/lib/constants/document.ts msgid "Pending" msgstr "Pendiente" @@ -3989,6 +4239,11 @@ msgstr "Por favor, revisa tu correo electrónico para actualizaciones." msgid "Please choose your new password" msgstr "Por favor, elige tu nueva contraseña" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +msgid "Please configure the document first" +msgstr "" + #: packages/lib/server-only/auth/send-confirmation-email.ts msgid "Please confirm your email" msgstr "Por favor confirma tu correo electrónico" @@ -4261,6 +4516,7 @@ msgstr "Destinatario actualizado" #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx #: apps/remix/app/components/general/template/template-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx msgid "Recipients" msgstr "Destinatarios" @@ -4284,6 +4540,7 @@ msgstr "Códigos de recuperación" msgid "Red" msgstr "Rojo" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Redirect URL" @@ -4345,7 +4602,6 @@ msgstr "Recordatorio: Por favor {recipientActionVerb} tu documento" #: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx #: apps/remix/app/components/general/teams/team-email-dropdown.tsx #: apps/remix/app/components/general/document-signing/document-signing-field-container.tsx -#: apps/remix/app/components/general/document-signing/document-signing-field-container.tsx #: apps/remix/app/components/forms/team-branding-preferences-form.tsx #: apps/remix/app/components/forms/avatar-image.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx @@ -4442,7 +4698,7 @@ msgstr "Retención de Documentos" msgid "Retry" msgstr "Reintentar" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.decline.$token.tsx @@ -4473,6 +4729,7 @@ msgstr "Revocar acceso" #: apps/remix/app/components/tables/user-settings-current-teams-table.tsx #: apps/remix/app/components/tables/team-settings-members-table.tsx #: apps/remix/app/components/tables/team-settings-member-invites-table.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx #: apps/remix/app/components/dialogs/team-member-invite-dialog.tsx @@ -4484,12 +4741,19 @@ msgstr "Rol" msgid "Roles" msgstr "Roles" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Root (No Folder)" +msgstr "" + #: packages/ui/primitives/data-table-pagination.tsx msgid "Rows per page" msgstr "Filas por página" #: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx #: apps/remix/app/components/general/document-signing/document-signing-number-field.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx msgid "Save" @@ -4544,6 +4808,10 @@ msgstr "Actividad de seguridad" msgid "Select" msgstr "Seleccionar" +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Select a folder to move this document to." +msgstr "" + #: apps/remix/app/components/dialogs/template-move-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Select a team" @@ -4665,6 +4933,14 @@ msgstr "Enviado" msgid "Set a password" msgstr "Establecer una contraseña" +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Set up your document properties and recipient information" +msgstr "" + +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Set up your template properties and recipient information" +msgstr "" + #: apps/remix/app/routes/_authenticated+/settings+/_layout.tsx #: apps/remix/app/components/general/app-nav-mobile.tsx #: apps/remix/app/components/general/app-command-menu.tsx @@ -4806,7 +5082,6 @@ msgstr "Regístrate con OIDC" #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/types.ts -#: packages/ui/primitives/document-flow/field-icon.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Signature" msgstr "Firma" @@ -4836,8 +5111,8 @@ msgid "Signatures will appear once the document has been completed" msgstr "Las firmas aparecerán una vez que el documento se haya completado" #: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx -#: apps/remix/app/components/general/document/document-read-only-fields.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx +#: packages/ui/components/document/document-read-only-fields.tsx #: packages/lib/constants/recipient-roles.ts msgid "Signed" msgstr "Firmado" @@ -4934,7 +5209,9 @@ msgstr "Algunos firmantes no han sido asignados a un campo de firma. Asigne al m #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx +#: apps/remix/app/components/general/share-document-download-button.tsx #: apps/remix/app/components/general/billing-portal-button.tsx #: apps/remix/app/components/general/billing-plans.tsx #: apps/remix/app/components/general/teams/team-layout-billing-banner.tsx @@ -4946,6 +5223,7 @@ msgstr "Algunos firmantes no han sido asignados a un campo de firma. Asigne al m #: apps/remix/app/components/general/document-signing/document-signing-auth-page.tsx #: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx #: apps/remix/app/components/general/document/document-page-view-dropdown.tsx +#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx #: apps/remix/app/components/general/document/document-certificate-download-button.tsx #: apps/remix/app/components/general/document/document-audit-log-download-button.tsx @@ -4972,7 +5250,7 @@ msgid "Something went wrong" msgstr "Algo salió mal" #. placeholder {0}: data.teamName -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Something went wrong while attempting to transfer the ownership of team <0>{0} to your. Please try again later or contact support." msgstr "Algo salió mal al intentar transferir la propiedad del equipo <0>{0} a tu equipo. Por favor, intenta de nuevo más tarde o contacta al soporte." @@ -5044,6 +5322,7 @@ msgstr "Estado" msgid "Step <0>{step} of {maxStep}" msgstr "Paso <0>{step} de {maxStep}" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-subject.tsx msgid "Subject <0>(Optional)" @@ -5061,6 +5340,8 @@ msgstr "Suscripción" msgid "Subscriptions" msgstr "Suscripciones" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx #: apps/remix/app/components/tables/user-settings-pending-teams-table-actions.tsx #: apps/remix/app/components/tables/team-settings-member-invites-table.tsx @@ -5201,15 +5482,15 @@ msgstr "Solo equipo" msgid "Team only templates are not linked anywhere and are visible only to your team." msgstr "Las plantillas solo para el equipo no están vinculadas en ningún lado y son visibles solo para tu equipo." -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Team ownership transfer" msgstr "Transferencia de propiedad del equipo" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Team ownership transfer already completed!" msgstr "¡La transferencia de propiedad del equipo ya se ha completado!" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Team ownership transferred!" msgstr "¡Propiedad del equipo transferida!" @@ -5258,6 +5539,8 @@ msgstr "Equipos" msgid "Teams restricted" msgstr "Equipos restringidos" +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id.edit.tsx +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id.edit.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx @@ -5268,6 +5551,10 @@ msgstr "Equipos restringidos" msgid "Template" msgstr "Plantilla" +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Template Created" +msgstr "" + #: apps/remix/app/components/dialogs/template-delete-dialog.tsx msgid "Template deleted" msgstr "Plantilla eliminada" @@ -5288,6 +5575,11 @@ msgstr "La plantilla ha sido eliminada de tu perfil público." msgid "Template has been updated." msgstr "La plantilla ha sido actualizada." +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Template is using legacy field insertion" +msgstr "" + +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "Template moved" msgstr "Plantilla movida" @@ -5304,6 +5596,12 @@ msgstr "Plantilla guardada" msgid "Template title" msgstr "Título de plantilla" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +msgid "Template updated successfully" +msgstr "" + +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx #: apps/remix/app/components/general/app-nav-mobile.tsx @@ -5379,10 +5677,18 @@ msgstr "El contenido que se mostrará en el banner, se permite HTML" msgid "The direct link has been copied to your clipboard" msgstr "El enlace directo ha sido copiado a tu portapapeles" +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "The document has been moved successfully." +msgstr "" + #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "The document has been successfully moved to the selected team." msgstr "El documento ha sido movido con éxito al equipo seleccionado." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "The document is already saved and cannot be changed." +msgstr "" + #: apps/remix/app/components/embed/embed-document-completed.tsx msgid "The document is now completed, please follow any instructions provided within the parent application." msgstr "El documento ahora está completado, por favor sigue cualquier instrucción proporcionada dentro de la aplicación principal." @@ -5421,6 +5727,28 @@ msgstr "El correo electrónico o la contraseña proporcionada es incorrecta" msgid "The events that will trigger a webhook to be sent to your URL." msgstr "Los eventos que activarán un webhook para ser enviado a tu URL." +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "The fields have been updated to the new field insertion method successfully" +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx +msgid "The folder you are trying to delete does not exist." +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx +#: apps/remix/app/components/dialogs/folder-move-dialog.tsx +msgid "The folder you are trying to move does not exist." +msgstr "" + +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "The folder you are trying to move the document to does not exist." +msgstr "" + +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "The folder you are trying to move the template to does not exist." +msgstr "" + #: packages/email/templates/bulk-send-complete.tsx msgid "The following errors occurred:" msgstr "Se produjeron los siguientes errores:" @@ -5434,7 +5762,7 @@ msgid "The following team has been deleted by you" msgstr "El siguiente equipo ha sido eliminado por ti" #. placeholder {0}: data.teamName -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "The ownership of team <0>{0} has been successfully transferred to you." msgstr "La propiedad del equipo <0>{0} ha sido transferida con éxito a ti." @@ -5531,11 +5859,17 @@ msgid "The team transfer request to <0>{0} has expired." msgstr "La solicitud de transferencia de equipo a <0>{0} ha expirado." #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/_layout.tsx -msgid "The team you are looking for may have been removed, renamed or may have never\n" +msgid "" +"The team you are looking for may have been removed, renamed or may have never\n" " existed." -msgstr "El equipo que buscas puede haber sido eliminado, renombrado o quizás nunca\n" +msgstr "" +"El equipo que buscas puede haber sido eliminado, renombrado o quizás nunca\n" " existió." +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "The template has been moved successfully." +msgstr "" + #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "The template has been successfully moved to the selected team." msgstr "La plantilla ha sido movida con éxito al equipo seleccionado." @@ -5591,6 +5925,10 @@ msgstr "No hay borradores activos en este momento. Puedes subir un documento par msgid "There are no completed documents yet. Documents that you have created or received will appear here once completed." msgstr "Aún no hay documentos completados. Los documentos que hayas creado o recibido aparecerán aquí una vez completados." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "There was an error uploading your file. Please try again." +msgstr "" + #: apps/remix/app/components/general/teams/team-email-usage.tsx msgid "They have permission on your behalf to:" msgstr "Tienen permiso en tu nombre para:" @@ -5621,6 +5959,10 @@ msgstr "Esto se puede anular configurando los requisitos de autenticación direc msgid "This document can not be recovered, if you would like to dispute the reason for future documents please contact support." msgstr "Este documento no se puede recuperar, si deseas impugnar la razón para documentos futuros, por favor contacta con el soporte." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "This document cannot be changed" +msgstr "" + #: apps/remix/app/components/dialogs/document-delete-dialog.tsx msgid "This document could not be deleted at this time. Please try again." msgstr "Este documento no se pudo eliminar en este momento. Por favor, inténtalo de nuevo." @@ -5633,7 +5975,7 @@ msgstr "Este documento no se pudo duplicar en este momento. Por favor, inténtal msgid "This document could not be re-sent at this time. Please try again." msgstr "Este documento no se pudo reenviar en este momento. Por favor, inténtalo de nuevo." -#: packages/ui/primitives/document-flow/add-fields.tsx +#: packages/ui/primitives/recipient-selector.tsx msgid "This document has already been sent to this recipient. You can no longer edit this recipient." msgstr "Este documento ya ha sido enviado a este destinatario. Ya no puede editar a este destinatario." @@ -5645,18 +5987,29 @@ msgstr "Este documento ha sido cancelado por el propietario y ya no está dispon msgid "This document has been cancelled by the owner." msgstr "Este documento ha sido cancelado por el propietario." +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "This document has been rejected by a recipient" msgstr "Este documento ha sido rechazado por un destinatario" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "This document has been signed by all recipients" msgstr "Este documento ha sido firmado por todos los destinatarios" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "This document is available in your Documenso account. You can view more details, recipients, and audit logs there." +msgstr "" + +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "This document is currently a draft and has not been sent" msgstr "Este documento es actualmente un borrador y no ha sido enviado" +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "This document is using legacy field insertion, we recommend using the new field insertion method for more accurate results." +msgstr "" + #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx msgid "This document was created by you or a team member using the template above." msgstr "Este documento fue creado por ti o un miembro del equipo usando la plantilla anterior." @@ -5697,11 +6050,16 @@ msgstr "Este correo electrónico se enviará al destinatario que acaba de firmar msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them." msgstr "Este campo no se puede modificar ni eliminar. Cuando comparta el enlace directo de esta plantilla o lo agregue a su perfil público, cualquiera que acceda podrá ingresar su nombre y correo electrónico, y completar los campos que se le hayan asignado." +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +#: apps/remix/app/components/dialogs/folder-create-dialog.tsx +msgid "This folder name is already taken." +msgstr "" + #: packages/ui/primitives/template-flow/add-template-settings.tsx msgid "This is how the document will reach the recipients once the document is ready for signing." msgstr "Así es como el documento llegará a los destinatarios una vez que esté listo para firmarse." -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "This link is invalid or has expired. Please contact your team to resend a transfer request." msgstr "Este enlace es inválido o ha expirado. Por favor, contacta a tu equipo para reenviar una solicitud de transferencia." @@ -5741,6 +6099,10 @@ msgstr "Este equipo, y cualquier dato asociado, excluyendo las facturas de factu msgid "This template could not be deleted at this time. Please try again." msgstr "Esta plantilla no se pudo eliminar en este momento. Por favor, inténtalo de nuevo." +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "This template is using legacy field insertion, we recommend using the new field insertion method for more accurate results." +msgstr "" + #: apps/remix/app/routes/_unauthenticated+/team.decline.$token.tsx msgid "This token is invalid or has expired. No action is needed." msgstr "Este token es inválido o ha expirado. No se necesita ninguna acción." @@ -5779,11 +6141,13 @@ msgstr "Esto anulará cualquier configuración global." msgid "Time" msgstr "Hora" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Time zone" msgstr "Zona horaria" #: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Time Zone" @@ -5793,6 +6157,7 @@ msgstr "Zona horaria" #: apps/remix/app/components/tables/templates-table.tsx #: apps/remix/app/components/tables/documents-table.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Title" msgstr "Título" @@ -6102,6 +6467,10 @@ msgstr "Actualizar" msgid "Update Banner" msgstr "Actualizar banner" +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Update Fields" +msgstr "" + #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx msgid "Update passkey" msgstr "Actualizar clave de acceso" @@ -6116,6 +6485,7 @@ msgstr "Actualizar perfil" #: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx +#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx msgid "Update Recipient" msgstr "Actualizar destinatario" @@ -6158,10 +6528,15 @@ msgstr "Actualizando contraseña..." msgid "Updating Your Information" msgstr "Actualizando Su Información" +#: packages/ui/primitives/document-upload.tsx #: packages/ui/primitives/document-dropzone.tsx msgid "Upgrade" msgstr "Actualizar" +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +msgid "Upgrade your plan to upload more documents" +msgstr "" + #: packages/lib/constants/document.ts msgid "Upload" msgstr "Subir" @@ -6190,10 +6565,17 @@ msgstr "Subir CSV" msgid "Upload custom document" msgstr "Subir documento personalizado" +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +#: packages/ui/primitives/document-upload.tsx +msgid "Upload Document" +msgstr "" + #: packages/ui/primitives/signature-pad/signature-pad-upload.tsx msgid "Upload Signature" msgstr "Subir firma" +#: packages/ui/primitives/document-upload.tsx #: packages/ui/primitives/document-dropzone.tsx msgid "Upload Template Document" msgstr "Cargar Documento Plantilla" @@ -6219,6 +6601,11 @@ msgstr "El archivo subido es demasiado pequeño" msgid "Uploaded file not an allowed file type" msgstr "El archivo subido no es un tipo de archivo permitido" +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +msgid "Uploading document..." +msgstr "" + +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Use" msgstr "Usar" @@ -6667,6 +7054,7 @@ msgstr "Generaremos enlaces de firma para ti, que podrás enviar a los destinata msgid "We won't send anything to notify recipients." msgstr "No enviaremos nada para notificar a los destinatarios." +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx #: apps/remix/app/routes/_authenticated+/templates._index.tsx #: apps/remix/app/components/tables/documents-table-empty-state.tsx msgid "We're all empty" @@ -6731,6 +7119,7 @@ msgstr "¡Bienvenido a Documenso!" msgid "Were you trying to edit this document instead?" msgstr "¿Estabas intentando editar este documento en su lugar?" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "When enabled, signers can choose who should sign next in the sequence instead of following the predefined order." @@ -6890,11 +7279,13 @@ msgstr "No puedes tener más de {MAXIMUM_PASSKEYS} claves de acceso." msgid "You cannot modify a team member who has a higher role than you." msgstr "No puedes modificar a un miembro del equipo que tenga un rol más alto que tú." +#: packages/ui/primitives/document-upload.tsx #: packages/ui/primitives/document-dropzone.tsx msgid "You cannot upload documents at this time." msgstr "No puede cargar documentos en este momento." #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "You cannot upload encrypted PDFs" msgstr "No puedes subir PDFs encriptados" @@ -6916,7 +7307,7 @@ msgid "You have accepted an invitation from <0>{0} to join their team." msgstr "Has aceptado una invitación de <0>{0} para unirte a su equipo." #. placeholder {0}: data.teamName -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "You have already completed the ownership transfer for <0>{0}." msgstr "Ya has completado la transferencia de propiedad para <0>{0}." @@ -6966,6 +7357,7 @@ msgstr "Has iniciado el documento {0} que requiere que {recipientActionVerb}." msgid "You have no webhooks yet. Your webhooks will be shown here once you create them." msgstr "Aún no tienes webhooks. Tus webhooks se mostrarán aquí una vez que los crees." +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx #: apps/remix/app/routes/_authenticated+/templates._index.tsx msgid "You have not yet created any templates. To create a template please upload one." msgstr "Aún no has creado plantillas. Para crear una plantilla, por favor carga una." @@ -6980,6 +7372,7 @@ msgid "You have reached the maximum limit of {0} direct templates. <0>Upgrade yo msgstr "Has alcanzado el límite máximo de {0} plantillas directas. <0>¡Actualiza tu cuenta para continuar!" #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "You have reached your document limit for this month. Please upgrade your plan." msgstr "Ha alcanzado su límite de documentos para este mes. Por favor, actualice su plan." @@ -7053,6 +7446,11 @@ msgstr "Debes tener al menos otro miembro del equipo para transferir la propieda msgid "You must set a profile URL before enabling your public profile." msgstr "Debes establecer una URL de perfil antes de habilitar tu perfil público." +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx +msgid "You must type '{deleteMessage}' to confirm" +msgstr "" + #: apps/remix/app/routes/_authenticated+/settings+/tokens.tsx msgid "You need to be an admin to manage API tokens." msgstr "Necesitas ser administrador para gestionar tokens de API." @@ -7118,6 +7516,8 @@ msgid "Your direct signing templates" msgstr "Tus {0} plantillas de firma directa" #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx msgid "Your document failed to upload." msgstr "Tu documento no se pudo cargar." @@ -7125,6 +7525,10 @@ msgstr "Tu documento no se pudo cargar." msgid "Your document has been created from the template successfully." msgstr "Tu documento se ha creado exitosamente a partir de la plantilla." +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Your document has been created successfully" +msgstr "" + #: packages/email/template-components/template-document-super-delete.tsx msgid "Your document has been deleted by an admin!" msgstr "¡Tu documento ha sido eliminado por un administrador!" @@ -7142,6 +7546,7 @@ msgid "Your document has been successfully duplicated." msgstr "Tu documento ha sido duplicado con éxito." #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "Your document has been uploaded successfully." msgstr "Tu documento ha sido subido con éxito." @@ -7235,6 +7640,10 @@ msgstr "Tu equipo ha sido eliminado con éxito." msgid "Your team has been successfully updated." msgstr "Tu equipo ha sido actualizado con éxito." +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Your template has been created successfully" +msgstr "" + #: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx msgid "Your template has been duplicated successfully." msgstr "Tu plantilla ha sido duplicada con éxito." @@ -7262,4 +7671,3 @@ msgstr "¡Tu token se creó con éxito! ¡Asegúrate de copiarlo porque no podr #: apps/remix/app/routes/_authenticated+/settings+/tokens.tsx msgid "Your tokens will be shown here once you create them." msgstr "Tus tokens se mostrarán aquí una vez que los crees." - diff --git a/packages/lib/translations/fr/web.po b/packages/lib/translations/fr/web.po index eaacb022f..b03d249e0 100644 --- a/packages/lib/translations/fr/web.po +++ b/packages/lib/translations/fr/web.po @@ -26,6 +26,10 @@ msgstr " Activer la signature de lien direct" msgid " The events that will trigger a webhook to be sent to your URL." msgstr " Les événements qui déclencheront l'envoi d'un webhook vers votre URL." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid ".PDF documents accepted (max {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB)" +msgstr "" + #. placeholder {0}: team.name #: apps/remix/app/components/forms/team-document-preferences-form.tsx msgid "\"{0}\" has invited you to sign \"example document\"." @@ -96,11 +100,13 @@ msgid "{0, plural, one {1 matching field} other {# matching fields}}" msgstr "{0, plural, one {1 champ correspondant} other {# champs correspondants}}" #. placeholder {0}: recipients.length +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.edit.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.edit.tsx msgid "{0, plural, one {1 Recipient} other {# Recipients}}" msgstr "{0, plural, one {1 Destinataire} other {# Destinataires}}" #. placeholder {0}: pendingRecipients.length +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}" msgstr "{0, plural, one {En attente d'1 destinataire} other {En attente de # destinataires}}" @@ -144,6 +150,7 @@ msgstr "{0} a quitté l'équipe {teamName} sur Documenso" #. placeholder {0}: remaining.documents #. placeholder {1}: quota.documents #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "{0} of {1} documents remaining this month." msgstr "{0} des {1} documents restants ce mois-ci." @@ -161,6 +168,7 @@ msgid "{0} on behalf of \"{1}\" has invited you to {recipientActionVerb} the doc msgstr "{0} représentant \"{1}\" vous a invité à {recipientActionVerb} le document \"{2}\"." #. placeholder {0}: recipients.length +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "{0} Recipient(s)" msgstr "{0} Destinataire(s)" @@ -327,6 +335,10 @@ msgstr "{recipientActionVerb} document" msgid "{recipientActionVerb} the document to complete the process." msgstr "{recipientActionVerb} the document to complete the process." +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "{recipientCount} recipients" +msgstr "" + #: packages/email/templates/document-created-from-direct-template.tsx 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" @@ -727,6 +739,7 @@ msgstr "Ajouter" msgid "Add a document" msgstr "Ajouter un document" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Add a URL to redirect the user to once the document is signed" @@ -800,6 +813,7 @@ msgstr "Ajouter un destinataire fictif" msgid "Add Placeholders" msgstr "Ajouter des espaces réservés" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Add Signer" msgstr "Ajouter un signataire" @@ -808,6 +822,10 @@ msgstr "Ajouter un signataire" msgid "Add Signers" msgstr "Ajouter des signataires" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +msgid "Add signers and configure signing preferences" +msgstr "" + #: apps/remix/app/components/dialogs/team-email-add-dialog.tsx msgid "Add team email" msgstr "Ajouter un e-mail d'équipe" @@ -853,11 +871,16 @@ msgstr "Panneau d'administration" msgid "Advanced Options" msgstr "Options avancées" +#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Advanced settings" msgstr "Paramètres avancés" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Advanced Settings" +msgstr "" + #: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx msgid "After signing a document electronically, you will be provided the opportunity to view, download, and print the document for your records. It is highly recommended that you retain a copy of all electronically signed documents for your personal records. We will also retain a copy of the signed document for our records however we may not be able to provide you with a copy of the signed document after a certain period of time." msgstr "Après avoir signé un document électroniquement, vous aurez l'occasion de visualiser, télécharger et imprimer le document pour vos dossiers. Il est fortement recommandé de conserver une copie de tous les documents signés électroniquement pour vos dossiers personnels. Nous conserverons également une copie du document signé pour nos dossiers, mais nous pourrions ne pas être en mesure de vous fournir une copie du document signé après une certaine période." @@ -882,6 +905,11 @@ msgstr "Tous les documents ont été traités. Tous nouveaux documents envoyés msgid "All documents related to the electronic signing process will be provided to you electronically through our platform or via email. It is your responsibility to ensure that your email address is current and that you can receive and open our emails." msgstr "Tous les documents relatifs au processus de signature électronique vous seront fournis électroniquement via notre plateforme ou par e-mail. Il est de votre responsabilité de vous assurer que votre adresse e-mail est à jour et que vous pouvez recevoir et ouvrir nos e-mails." +#: apps/remix/app/routes/_authenticated+/templates.folders._index.tsx +#: apps/remix/app/routes/_authenticated+/documents.folders._index.tsx +msgid "All Folders" +msgstr "" + #: apps/remix/app/components/dialogs/document-delete-dialog.tsx msgid "All inserted signatures will be voided" msgstr "Toutes les signatures insérées seront annulées" @@ -910,11 +938,13 @@ msgstr "Depuis toujours" msgid "Allow document recipients to reply directly to this email address" msgstr "Autoriser les destinataires du document à répondre directement à cette adresse e-mail" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Allow signers to dictate next signer" msgstr "Permettre aux signataires de dicter le prochain signataire" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Allowed Signature Types" @@ -998,8 +1028,11 @@ msgstr "Une erreur est survenue lors de la désactivation de la signature par li msgid "An error occurred while disabling the user." msgstr "Une erreur est survenue lors de la désactivation de l'utilisateur." +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx +#: apps/remix/app/components/general/share-document-download-button.tsx +#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx msgid "An error occurred while downloading your document." @@ -1025,10 +1058,12 @@ msgstr "Une erreur est survenue lors du chargement des membres de l'équipe. Veu msgid "An error occurred while loading the document." msgstr "Une erreur est survenue lors du chargement du document." +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "An error occurred while moving the document." msgstr "Une erreur est survenue lors du déplacement du document." +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "An error occurred while moving the template." msgstr "Une erreur est survenue lors du déplacement du modèle." @@ -1103,6 +1138,7 @@ msgid "An error occurred while updating your profile." msgstr "Une erreur est survenue lors de la mise à jour de votre profil." #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "An error occurred while uploading your document." msgstr "Une erreur est survenue lors de l'importation de votre document." @@ -1140,6 +1176,21 @@ msgstr "Une erreur inattendue est survenue." msgid "An unknown error occurred" msgstr "Une erreur inconnue est survenue" +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +#: apps/remix/app/components/dialogs/folder-create-dialog.tsx +msgid "An unknown error occurred while creating the folder." +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx +msgid "An unknown error occurred while deleting the folder." +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx +#: apps/remix/app/components/dialogs/folder-move-dialog.tsx +msgid "An unknown error occurred while moving the folder." +msgstr "" + #: apps/remix/app/components/dialogs/team-transfer-dialog.tsx msgid "Any payment methods attached to this team will remain attached to this team. Please contact us if you need to update this information." msgstr "Tous les moyens de paiement associés à cette équipe resteront associés à cette équipe. Veuillez nous contacter si vous avez besoin de mettre à jour ces informations." @@ -1297,6 +1348,8 @@ msgstr "En attente de confirmation par e-mail" #: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx #: apps/remix/app/components/forms/signup.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx msgid "Back" msgstr "Retour" @@ -1447,6 +1500,7 @@ msgstr "Peut préparer" #: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx #: apps/remix/app/components/dialogs/webhook-create-dialog.tsx #: apps/remix/app/components/dialogs/token-delete-dialog.tsx +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx #: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx @@ -1466,6 +1520,7 @@ msgstr "Peut préparer" #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx #: apps/remix/app/components/dialogs/passkey-create-dialog.tsx #: apps/remix/app/components/dialogs/document-resend-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx #: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx #: apps/remix/app/components/dialogs/document-delete-dialog.tsx @@ -1480,6 +1535,10 @@ msgstr "Annuler" msgid "Cancelled by user" msgstr "Annulé par l'utilisateur" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Cannot remove document" +msgstr "" + #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Cannot remove signer" msgstr "Impossible de supprimer le signataire" @@ -1533,6 +1592,10 @@ msgstr "Choisissez un destinataire pour le lien direct" msgid "Choose how the document will reach recipients" msgstr "Choisissez comment le document atteindra les destinataires" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Choose how to distribute your document to recipients. Email will send notifications, None will generate signing links for manual distribution." +msgstr "" + #: apps/remix/app/components/forms/token.tsx msgid "Choose..." msgstr "Choisissez..." @@ -1602,6 +1665,10 @@ msgstr "Cliquez pour insérer un champ" msgid "Close" msgstr "Fermer" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Communication" +msgstr "" + #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/forms/signup.tsx @@ -1658,10 +1725,33 @@ msgstr "Documents complétés" msgid "Completed Documents" msgstr "Documents Complétés" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "Completed on {formattedDate}" +msgstr "" + +#. placeholder {0}: parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[currentField.type]) +#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx +msgid "Configure {0} Field" +msgstr "" + +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Configure additional options and preferences" +msgstr "" + #: packages/lib/constants/template.ts msgid "Configure Direct Recipient" msgstr "Configurer le destinataire direct" +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Configure Document" +msgstr "" + +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +msgid "Configure Fields" +msgstr "" + #: apps/remix/app/components/general/document/document-edit-form.tsx msgid "Configure general settings for the document." msgstr "Configurer les paramètres généraux pour le document." @@ -1674,12 +1764,22 @@ msgstr "Configurer les paramètres généraux pour le modèle." msgid "Configure template" msgstr "Configurer le modèle" +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Configure Template" +msgstr "" + #. placeholder {0}: parseMessageDescriptor( _, FRIENDLY_FIELD_TYPE[currentField.type], ) +#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Configure the {0} field" msgstr "Configurer le champ {0}" +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +msgid "Configure the fields you want to place on the document." +msgstr "" + #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx msgid "Confirm" @@ -1695,6 +1795,8 @@ msgstr "Confirmer en tapant <0>{deleteMessage}" #: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx #: apps/remix/app/components/dialogs/token-delete-dialog.tsx +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx msgid "Confirm by typing: <0>{deleteMessage}" msgstr "Confirmer en tapant : <0>{deleteMessage}" @@ -1724,12 +1826,13 @@ msgid "Content" msgstr "Contenu" #: apps/remix/app/routes/_unauthenticated+/verify-email.$token.tsx -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx #: apps/remix/app/components/general/document-signing/document-signing-form.tsx +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx #: apps/remix/app/components/dialogs/passkey-create-dialog.tsx #: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx @@ -1940,6 +2043,7 @@ msgstr "Créé" msgid "Created At" msgstr "Créé le" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Created by" msgstr "Créé par" @@ -1989,10 +2093,12 @@ msgstr "Mode sombre" msgid "Date" msgstr "Date" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Date created" msgstr "Date de création" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Date Format" @@ -2042,8 +2148,11 @@ msgstr "Supprimer" #. placeholder {0}: webhook.webhookUrl #. placeholder {0}: token.name +#. placeholder {0}: folder?.name ?? 'folder' #: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx #: apps/remix/app/components/dialogs/token-delete-dialog.tsx +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx msgid "delete {0}" msgstr "supprimer {0}" @@ -2217,6 +2326,10 @@ msgstr "Afficher votre nom et votre email dans les documents" msgid "Distribute Document" msgstr "Distribuer le document" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Distribution Method" +msgstr "" + #: apps/remix/app/components/dialogs/template-delete-dialog.tsx msgid "Do you want to delete this template?" msgstr "Voulez-vous supprimer ce modèle ?" @@ -2229,6 +2342,7 @@ msgstr "Voulez-vous dupliquer ce modèle ?" msgid "Documenso will delete <0>all of your documents, along with all of your completed documents, signatures, and all other resources belonging to your Account." msgstr "Documenso supprimera <0>tous vos documents, ainsi que tous vos documents complétés, signatures, et toutes les autres ressources appartenant à votre compte." +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx msgid "Document" @@ -2294,6 +2408,10 @@ msgstr "Document Complété !" msgid "Document created" msgstr "Document créé" +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Document Created" +msgstr "" + #. placeholder {0}: document.user.name #: apps/remix/app/components/general/template/template-page-view-recent-activity.tsx msgid "Document created by <0>{0}" @@ -2312,6 +2430,7 @@ msgstr "Document créé en utilisant un <0>lien direct" msgid "Document Creation" msgstr "Création de document" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx #: apps/remix/app/components/dialogs/document-delete-dialog.tsx #: apps/remix/app/components/dialogs/admin-document-delete-dialog.tsx @@ -2344,12 +2463,18 @@ msgstr "Document dupliqué" msgid "Document external ID updated" msgstr "ID externe du document mis à jour" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "Document found in your account" +msgstr "" + +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx #: apps/remix/app/components/general/document/document-history-sheet.tsx msgid "Document history" msgstr "Historique du document" #: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Document ID" msgstr "ID du document" @@ -2358,6 +2483,14 @@ msgstr "ID du document" msgid "Document inbox" msgstr "Boîte de réception des documents" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Document is already uploaded" +msgstr "" + +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Document is using legacy field insertion" +msgstr "" + #: apps/remix/app/components/tables/templates-table.tsx msgid "Document Limit Exceeded!" msgstr "Limite de documents dépassée !" @@ -2366,6 +2499,7 @@ msgstr "Limite de documents dépassée !" msgid "Document metrics" msgstr "Métriques du document" +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Document moved" msgstr "Document déplacé" @@ -2429,10 +2563,12 @@ msgstr "Authentification de signature de document mise à jour" msgid "Document signing process will be cancelled" msgstr "Le processus de signature du document sera annulé" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Document status" msgstr "Statut du document" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Document title" msgstr "Titre du document" @@ -2445,11 +2581,16 @@ msgstr "Titre du document mis à jour" msgid "Document updated" msgstr "Document mis à jour" +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +msgid "Document updated successfully" +msgstr "" + #: apps/remix/app/components/general/document/document-upload.tsx msgid "Document upload disabled due to unpaid invoices" msgstr "Importation de documents désactivé en raison de factures impayées" #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "Document uploaded" msgstr "Document importé" @@ -2466,6 +2607,9 @@ msgid "Document will be permanently deleted" msgstr "Le document sera supprimé de manière permanente" #: apps/remix/app/routes/_profile+/p.$url.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId._index.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.edit.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.edit.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx @@ -2480,6 +2624,7 @@ msgstr "Le document sera supprimé de manière permanente" msgid "Documents" msgstr "Documents" +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Documents created from template" msgstr "Documents créés à partir du modèle" @@ -2500,6 +2645,7 @@ msgstr "Vous n'avez pas de compte? <0>Inscrivez-vous" #: apps/remix/app/components/tables/team-settings-billing-invoices-table.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx +#: apps/remix/app/components/general/share-document-download-button.tsx #: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx #: apps/remix/app/components/forms/2fa/view-recovery-codes-dialog.tsx @@ -2517,6 +2663,11 @@ msgstr "Télécharger les journaux d'audit" msgid "Download Certificate" msgstr "Télécharger le certificat" +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx +#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx +msgid "Download Original" +msgstr "" + #: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx msgid "Download Template CSV" msgstr "Télécharger le modèle CSV" @@ -2539,10 +2690,22 @@ msgstr "Documents brouillon" msgid "Drag & drop your PDF here." msgstr "Faites glisser et déposez votre PDF ici." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Drag and drop or click to upload" +msgstr "" + +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +msgid "Drag and drop your PDF file here" +msgstr "" + #: packages/lib/constants/document.ts msgid "Draw" msgstr "Dessiner" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Drop your document here" +msgstr "" + #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Dropdown" @@ -2577,6 +2740,7 @@ msgstr "Dupliquer" msgid "Edit" msgstr "Modifier" +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Edit Template" msgstr "Modifier le modèle" @@ -2607,6 +2771,9 @@ msgstr "Divulgation de signature électronique" #: apps/remix/app/components/forms/forgot-password.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx @@ -2700,6 +2867,7 @@ msgstr "Activer la personnalisation de la marque pour tous les documents de cett msgid "Enable Direct Link Signing" msgstr "Activer la signature de lien direct" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Enable signing order" @@ -2746,6 +2914,10 @@ msgstr "Entrez votre nom" msgid "Enter your text here" msgstr "Entrez votre texte ici" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx #: apps/remix/app/components/general/verify-email-banner.tsx @@ -2780,10 +2952,15 @@ msgstr "Entrez votre texte ici" #: apps/remix/app/components/general/document/document-edit-form.tsx #: apps/remix/app/components/general/document/document-edit-form.tsx #: apps/remix/app/components/general/document/document-edit-form.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx #: apps/remix/app/components/dialogs/webhook-create-dialog.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx #: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx #: apps/remix/app/components/dialogs/admin-user-enable-dialog.tsx #: apps/remix/app/components/dialogs/admin-user-disable-dialog.tsx @@ -2793,6 +2970,10 @@ msgstr "Entrez votre texte ici" msgid "Error" msgstr "Erreur" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Error uploading file" +msgstr "" + #: apps/remix/app/components/forms/team-document-preferences-form.tsx msgid "Everyone can access and view the document" msgstr "Tout le monde peut accéder et voir le document" @@ -2823,6 +3004,11 @@ msgstr "Expire le {0}" msgid "External ID" msgstr "ID externe" +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +msgid "Failed to create folder" +msgstr "" + #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx msgid "Failed to reseal document" msgstr "Échec du reseal du document" @@ -2831,10 +3017,18 @@ msgstr "Échec du reseal du document" msgid "Failed to save settings." msgstr "Échec de l'enregistrement des paramètres." +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +msgid "Failed to update document" +msgstr "" + #: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx msgid "Failed to update recipient" msgstr "Échec de la mise à jour du destinataire" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +msgid "Failed to update template" +msgstr "" + #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id.tsx #: apps/remix/app/routes/_authenticated+/settings+/webhooks.$id.tsx msgid "Failed to update webhook" @@ -2887,7 +3081,13 @@ msgstr "Champ non signé" msgid "Fields" msgstr "Champs" +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Fields updated" +msgstr "" + #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx msgid "File cannot be larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB" msgstr "Le fichier ne peut pas dépasser {APP_DOCUMENT_UPLOAD_SIZE_LIMIT} Mo" @@ -2895,6 +3095,25 @@ msgstr "Le fichier ne peut pas dépasser {APP_DOCUMENT_UPLOAD_SIZE_LIMIT} Mo" msgid "File size exceeds the limit of {APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB" msgstr "La taille du fichier dépasse la limite de {APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Folder" +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +msgid "Folder created successfully" +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx +#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx +msgid "Folder not found" +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx +#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx +msgid "Folder updated successfully" +msgstr "" + #: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx #: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx #: packages/ui/primitives/document-flow/field-items-advanced-settings/name-field.tsx @@ -2941,6 +3160,7 @@ msgstr "Nom complet" #: apps/remix/app/components/general/teams/team-settings-nav-desktop.tsx #: apps/remix/app/components/general/document/document-edit-form.tsx #: apps/remix/app/components/general/direct-template/direct-template-page.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx msgid "General" msgstr "Général" @@ -2971,6 +3191,10 @@ msgstr "Revenir au tableau de bord" msgid "Go Back Home" msgstr "Revenir au tableau de bord" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "Go to document" +msgstr "" + #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx msgid "Go to owner" msgstr "Aller au propriétaire" @@ -3142,7 +3366,7 @@ msgstr "Code invalide. Veuillez réessayer." msgid "Invalid email" msgstr "Email invalide" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx msgid "Invalid link" msgstr "Lien invalide" @@ -3239,6 +3463,7 @@ msgid "Label" msgstr "Étiquette" #: apps/remix/app/components/general/menu-switcher.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Language" @@ -3261,6 +3486,7 @@ msgstr "7 derniers jours" msgid "Last modified" msgstr "Dernière modification" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Last updated" msgstr "Dernière mise à jour" @@ -3363,6 +3589,7 @@ msgstr "Gérer le profil de {0}" msgid "Manage all teams you are currently associated with." msgstr "Gérer toutes les équipes avec lesquelles vous êtes actuellement associé." +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Manage and view template" msgstr "Gérer et afficher le modèle" @@ -3471,6 +3698,7 @@ msgstr "Membre depuis" msgid "Members" msgstr "Membres" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-subject.tsx msgid "Message <0>(Optional)" @@ -3498,19 +3726,38 @@ msgstr "Utilisateurs actifs mensuels : utilisateurs ayant créé au moins un doc msgid "Monthly Active Users: Users that had at least one of their documents completed" msgstr "Utilisateurs actifs mensuels : utilisateurs ayant terminé au moins un de leurs documents" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Move" msgstr "Déplacer" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "Move \"{templateTitle}\" to a folder" +msgstr "" + +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Move Document to Folder" +msgstr "" + #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Move Document to Team" msgstr "Déplacer le document vers l'équipe" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "Move Template to Folder" +msgstr "" + #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "Move Template to Team" msgstr "Déplacer le modèle vers l'équipe" +#: apps/remix/app/components/tables/templates-table-action-dropdown.tsx +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx +msgid "Move to Folder" +msgstr "" + #: apps/remix/app/components/tables/templates-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx msgid "Move to Team" @@ -3534,6 +3781,8 @@ msgstr "Mes modèles" #: apps/remix/app/components/general/claim-account.tsx #: apps/remix/app/components/general/document-signing/document-signing-name-field.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx @@ -3622,8 +3871,8 @@ msgstr "Aucune activité récente" msgid "No recent documents" msgstr "Aucun document récent" +#: packages/ui/primitives/recipient-selector.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx -#: packages/ui/primitives/document-flow/add-fields.tsx msgid "No recipient matching this description was found." msgstr "Aucun destinataire correspondant à cette description n'a été trouvé." @@ -3634,8 +3883,8 @@ msgstr "Aucun destinataire correspondant à cette description n'a été trouvé. msgid "No recipients" msgstr "Aucun destinataire" +#: packages/ui/primitives/recipient-selector.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx -#: packages/ui/primitives/document-flow/add-fields.tsx msgid "No recipients with this role" msgstr "Aucun destinataire avec ce rôle" @@ -3677,6 +3926,7 @@ msgstr "Aucune valeur trouvée." msgid "No worries, it happens! Enter your email and we'll email you a special link to reset your password." msgstr "Pas de soucis, ça arrive ! Entrez votre email et nous vous enverrons un lien spécial pour réinitialiser votre mot de passe." +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/lib/constants/document.ts msgid "None" msgstr "Aucun" @@ -3900,8 +4150,8 @@ msgstr "Paiement en retard" #: apps/remix/app/components/tables/user-settings-teams-page-table.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx #: apps/remix/app/components/general/document/document-status.tsx -#: apps/remix/app/components/general/document/document-read-only-fields.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx +#: packages/ui/components/document/document-read-only-fields.tsx #: packages/lib/constants/document.ts msgid "Pending" msgstr "En attente" @@ -3989,6 +4239,11 @@ msgstr "Veuillez vérifier votre e-mail pour des mises à jour." msgid "Please choose your new password" msgstr "Veuillez choisir votre nouveau mot de passe" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +msgid "Please configure the document first" +msgstr "" + #: packages/lib/server-only/auth/send-confirmation-email.ts msgid "Please confirm your email" msgstr "Veuillez confirmer votre email" @@ -4261,6 +4516,7 @@ msgstr "Destinataire mis à jour" #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx #: apps/remix/app/components/general/template/template-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx msgid "Recipients" msgstr "Destinataires" @@ -4284,6 +4540,7 @@ msgstr "Codes de récupération" msgid "Red" msgstr "Rouge" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Redirect URL" @@ -4345,7 +4602,6 @@ msgstr "Rappel : Veuillez {recipientActionVerb} votre document" #: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx #: apps/remix/app/components/general/teams/team-email-dropdown.tsx #: apps/remix/app/components/general/document-signing/document-signing-field-container.tsx -#: apps/remix/app/components/general/document-signing/document-signing-field-container.tsx #: apps/remix/app/components/forms/team-branding-preferences-form.tsx #: apps/remix/app/components/forms/avatar-image.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx @@ -4442,7 +4698,7 @@ msgstr "Conservation des documents" msgid "Retry" msgstr "Réessayer" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.decline.$token.tsx @@ -4473,6 +4729,7 @@ msgstr "Révoquer l'accès" #: apps/remix/app/components/tables/user-settings-current-teams-table.tsx #: apps/remix/app/components/tables/team-settings-members-table.tsx #: apps/remix/app/components/tables/team-settings-member-invites-table.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx #: apps/remix/app/components/dialogs/team-member-invite-dialog.tsx @@ -4484,12 +4741,19 @@ msgstr "Rôle" msgid "Roles" msgstr "Rôles" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Root (No Folder)" +msgstr "" + #: packages/ui/primitives/data-table-pagination.tsx msgid "Rows per page" msgstr "Lignes par page" #: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx #: apps/remix/app/components/general/document-signing/document-signing-number-field.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx msgid "Save" @@ -4544,6 +4808,10 @@ msgstr "Activité de sécurité" msgid "Select" msgstr "Sélectionner" +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Select a folder to move this document to." +msgstr "" + #: apps/remix/app/components/dialogs/template-move-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Select a team" @@ -4665,6 +4933,14 @@ msgstr "Envoyé" msgid "Set a password" msgstr "Définir un mot de passe" +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Set up your document properties and recipient information" +msgstr "" + +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Set up your template properties and recipient information" +msgstr "" + #: apps/remix/app/routes/_authenticated+/settings+/_layout.tsx #: apps/remix/app/components/general/app-nav-mobile.tsx #: apps/remix/app/components/general/app-command-menu.tsx @@ -4806,7 +5082,6 @@ msgstr "S'inscrire avec OIDC" #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/types.ts -#: packages/ui/primitives/document-flow/field-icon.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Signature" msgstr "Signature" @@ -4836,8 +5111,8 @@ msgid "Signatures will appear once the document has been completed" msgstr "Les signatures apparaîtront une fois le document complété" #: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx -#: apps/remix/app/components/general/document/document-read-only-fields.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx +#: packages/ui/components/document/document-read-only-fields.tsx #: packages/lib/constants/recipient-roles.ts msgid "Signed" msgstr "Signé" @@ -4934,7 +5209,9 @@ msgstr "Certains signataires n'ont pas été assignés à un champ de signature. #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx +#: apps/remix/app/components/general/share-document-download-button.tsx #: apps/remix/app/components/general/billing-portal-button.tsx #: apps/remix/app/components/general/billing-plans.tsx #: apps/remix/app/components/general/teams/team-layout-billing-banner.tsx @@ -4946,6 +5223,7 @@ msgstr "Certains signataires n'ont pas été assignés à un champ de signature. #: apps/remix/app/components/general/document-signing/document-signing-auth-page.tsx #: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx #: apps/remix/app/components/general/document/document-page-view-dropdown.tsx +#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx #: apps/remix/app/components/general/document/document-certificate-download-button.tsx #: apps/remix/app/components/general/document/document-audit-log-download-button.tsx @@ -4972,7 +5250,7 @@ msgid "Something went wrong" msgstr "Quelque chose a mal tourné" #. placeholder {0}: data.teamName -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Something went wrong while attempting to transfer the ownership of team <0>{0} to your. Please try again later or contact support." msgstr "Quelque chose a mal tourné lors de la tentative de transfert de la propriété de l'équipe <0>{0} à vous. Veuillez réessayer plus tard ou contacter le support." @@ -5044,6 +5322,7 @@ msgstr "Statut" msgid "Step <0>{step} of {maxStep}" msgstr "Étape <0>{step} sur {maxStep}" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-subject.tsx msgid "Subject <0>(Optional)" @@ -5061,6 +5340,8 @@ msgstr "Abonnement" msgid "Subscriptions" msgstr "Abonnements" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx #: apps/remix/app/components/tables/user-settings-pending-teams-table-actions.tsx #: apps/remix/app/components/tables/team-settings-member-invites-table.tsx @@ -5201,15 +5482,15 @@ msgstr "Équipe uniquement" msgid "Team only templates are not linked anywhere and are visible only to your team." msgstr "Les modèles uniquement pour l'équipe ne sont liés nulle part et ne sont visibles que pour votre équipe." -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Team ownership transfer" msgstr "Transfert de propriété d'équipe" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Team ownership transfer already completed!" msgstr "Le transfert de propriété de l'équipe a déjà été effectué !" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Team ownership transferred!" msgstr "Propriété de l'équipe transférée !" @@ -5258,6 +5539,8 @@ msgstr "Équipes" msgid "Teams restricted" msgstr "Équipes restreintes" +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id.edit.tsx +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id.edit.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx @@ -5268,6 +5551,10 @@ msgstr "Équipes restreintes" msgid "Template" msgstr "Modèle" +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Template Created" +msgstr "" + #: apps/remix/app/components/dialogs/template-delete-dialog.tsx msgid "Template deleted" msgstr "Modèle supprimé" @@ -5288,6 +5575,11 @@ msgstr "Le modèle a été retiré de votre profil public." msgid "Template has been updated." msgstr "Le modèle a été mis à jour." +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Template is using legacy field insertion" +msgstr "" + +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "Template moved" msgstr "Modèle déplacé" @@ -5304,6 +5596,12 @@ msgstr "Modèle enregistré" msgid "Template title" msgstr "Titre du modèle" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +msgid "Template updated successfully" +msgstr "" + +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx #: apps/remix/app/components/general/app-nav-mobile.tsx @@ -5379,10 +5677,18 @@ msgstr "Le contenu à afficher dans la bannière, le HTML est autorisé" msgid "The direct link has been copied to your clipboard" msgstr "Le lien direct a été copié dans votre presse-papiers" +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "The document has been moved successfully." +msgstr "" + #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "The document has been successfully moved to the selected team." msgstr "Le document a été déplacé avec succès vers l'équipe sélectionnée." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "The document is already saved and cannot be changed." +msgstr "" + #: apps/remix/app/components/embed/embed-document-completed.tsx msgid "The document is now completed, please follow any instructions provided within the parent application." msgstr "Le document est maintenant complet, veuillez suivre toutes les instructions fournies dans l'application parente." @@ -5421,6 +5727,28 @@ msgstr "L'email ou le mot de passe fourni est incorrect" msgid "The events that will trigger a webhook to be sent to your URL." msgstr "Les événements qui déclencheront un webhook à envoyer à votre URL." +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "The fields have been updated to the new field insertion method successfully" +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx +msgid "The folder you are trying to delete does not exist." +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx +#: apps/remix/app/components/dialogs/folder-move-dialog.tsx +msgid "The folder you are trying to move does not exist." +msgstr "" + +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "The folder you are trying to move the document to does not exist." +msgstr "" + +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "The folder you are trying to move the template to does not exist." +msgstr "" + #: packages/email/templates/bulk-send-complete.tsx msgid "The following errors occurred:" msgstr "Les erreurs suivantes se sont produites :" @@ -5434,7 +5762,7 @@ msgid "The following team has been deleted by you" msgstr "L'équipe suivante a été supprimée par vous" #. placeholder {0}: data.teamName -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "The ownership of team <0>{0} has been successfully transferred to you." msgstr "La propriété de l'équipe <0>{0} a été transférée avec succès à vous." @@ -5531,10 +5859,15 @@ msgid "The team transfer request to <0>{0} has expired." msgstr "La demande de transfert d'équipe à <0>{0} a expiré." #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/_layout.tsx -msgid "The team you are looking for may have been removed, renamed or may have never\n" +msgid "" +"The team you are looking for may have been removed, renamed or may have never\n" " existed." msgstr "L'équipe que vous cherchez a peut-être été supprimée, renommée ou n'a peut-être jamais existé." +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "The template has been moved successfully." +msgstr "" + #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "The template has been successfully moved to the selected team." msgstr "Le modèle a été déplacé avec succès vers l'équipe sélectionnée." @@ -5590,6 +5923,10 @@ msgstr "Il n'y a pas de brouillons actifs pour le moment. Vous pouvez importer u msgid "There are no completed documents yet. Documents that you have created or received will appear here once completed." msgstr "Il n'y a pas encore de documents complétés. Les documents que vous avez créés ou reçus apparaîtront ici une fois complétés." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "There was an error uploading your file. Please try again." +msgstr "" + #: apps/remix/app/components/general/teams/team-email-usage.tsx msgid "They have permission on your behalf to:" msgstr "Ils ont la permission en votre nom de:" @@ -5620,6 +5957,10 @@ msgstr "Cela peut être remplacé par le paramétrage direct des exigences d'aut msgid "This document can not be recovered, if you would like to dispute the reason for future documents please contact support." msgstr "Ce document ne peut pas être récupéré, si vous souhaitez contester la raison des documents futurs, veuillez contacter le support." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "This document cannot be changed" +msgstr "" + #: apps/remix/app/components/dialogs/document-delete-dialog.tsx msgid "This document could not be deleted at this time. Please try again." msgstr "Ce document n'a pas pu être supprimé pour le moment. Veuillez réessayer." @@ -5632,7 +5973,7 @@ msgstr "Ce document n'a pas pu être dupliqué pour le moment. Veuillez réessay msgid "This document could not be re-sent at this time. Please try again." msgstr "Ce document n'a pas pu être renvoyé pour le moment. Veuillez réessayer." -#: packages/ui/primitives/document-flow/add-fields.tsx +#: packages/ui/primitives/recipient-selector.tsx msgid "This document has already been sent to this recipient. You can no longer edit this recipient." msgstr "Ce document a déjà été envoyé à ce destinataire. Vous ne pouvez plus modifier ce destinataire." @@ -5644,18 +5985,29 @@ msgstr "Ce document a été annulé par le propriétaire et n'est plus disponibl msgid "This document has been cancelled by the owner." msgstr "Ce document a été annulé par le propriétaire." +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "This document has been rejected by a recipient" msgstr "Ce document a été rejeté par un destinataire" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "This document has been signed by all recipients" msgstr "Ce document a été signé par tous les destinataires" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "This document is available in your Documenso account. You can view more details, recipients, and audit logs there." +msgstr "" + +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "This document is currently a draft and has not been sent" msgstr "Ce document est actuellement un brouillon et n'a pas été envoyé" +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "This document is using legacy field insertion, we recommend using the new field insertion method for more accurate results." +msgstr "" + #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx msgid "This document was created by you or a team member using the template above." msgstr "Ce document a été créé par vous ou un membre de l'équipe en utilisant le modèle ci-dessus." @@ -5696,11 +6048,16 @@ msgstr "Cet e-mail sera envoyé au destinataire qui vient de signer le document, msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them." msgstr "Ce champ ne peut pas être modifié ou supprimé. Lorsque vous partagez le lien direct de ce modèle ou l'ajoutez à votre profil public, toute personne qui y accède peut saisir son nom et son email, et remplir les champs qui lui sont attribués." +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +#: apps/remix/app/components/dialogs/folder-create-dialog.tsx +msgid "This folder name is already taken." +msgstr "" + #: packages/ui/primitives/template-flow/add-template-settings.tsx msgid "This is how the document will reach the recipients once the document is ready for signing." msgstr "Voici comment le document atteindra les destinataires une fois qu'il sera prêt à être signé." -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "This link is invalid or has expired. Please contact your team to resend a transfer request." msgstr "Ce lien est invalide ou a expiré. Veuillez contacter votre équipe pour renvoyer une demande de transfert." @@ -5740,6 +6097,10 @@ msgstr "Cette équipe, et toutes les données associées à l'exception des fact msgid "This template could not be deleted at this time. Please try again." msgstr "Ce modèle n'a pas pu être supprimé pour le moment. Veuillez réessayer." +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "This template is using legacy field insertion, we recommend using the new field insertion method for more accurate results." +msgstr "" + #: apps/remix/app/routes/_unauthenticated+/team.decline.$token.tsx msgid "This token is invalid or has expired. No action is needed." msgstr "Ce token est invalide ou a expiré. Aucune action n'est nécessaire." @@ -5778,11 +6139,13 @@ msgstr "Cela remplacera tous les paramètres globaux." msgid "Time" msgstr "Temps" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Time zone" msgstr "Fuseau horaire" #: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Time Zone" @@ -5792,6 +6155,7 @@ msgstr "Fuseau horaire" #: apps/remix/app/components/tables/templates-table.tsx #: apps/remix/app/components/tables/documents-table.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Title" msgstr "Titre" @@ -6101,6 +6465,10 @@ msgstr "Mettre à jour" msgid "Update Banner" msgstr "Mettre à jour la bannière" +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Update Fields" +msgstr "" + #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx msgid "Update passkey" msgstr "Mettre à jour la clé d'accès" @@ -6115,6 +6483,7 @@ msgstr "Mettre à jour le profil" #: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx +#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx msgid "Update Recipient" msgstr "Mettre à jour le destinataire" @@ -6157,10 +6526,15 @@ msgstr "Mise à jour du mot de passe..." msgid "Updating Your Information" msgstr "Mise à jour de vos informations" +#: packages/ui/primitives/document-upload.tsx #: packages/ui/primitives/document-dropzone.tsx msgid "Upgrade" msgstr "Améliorer" +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +msgid "Upgrade your plan to upload more documents" +msgstr "" + #: packages/lib/constants/document.ts msgid "Upload" msgstr "Télécharger" @@ -6189,10 +6563,17 @@ msgstr "Importer le CSV" msgid "Upload custom document" msgstr "Importer un document personnalisé" +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +#: packages/ui/primitives/document-upload.tsx +msgid "Upload Document" +msgstr "" + #: packages/ui/primitives/signature-pad/signature-pad-upload.tsx msgid "Upload Signature" msgstr "Importer une signature" +#: packages/ui/primitives/document-upload.tsx #: packages/ui/primitives/document-dropzone.tsx msgid "Upload Template Document" msgstr "Importer le document modèle" @@ -6218,6 +6599,11 @@ msgstr "Le fichier importé est trop petit" msgid "Uploaded file not an allowed file type" msgstr "Le fichier importé n'est pas un type de fichier autorisé" +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +msgid "Uploading document..." +msgstr "" + +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Use" msgstr "Utiliser" @@ -6666,6 +7052,7 @@ msgstr "Nous allons générer des liens de signature pour vous, que vous pouvez msgid "We won't send anything to notify recipients." msgstr "Nous n'enverrons rien pour notifier les destinataires." +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx #: apps/remix/app/routes/_authenticated+/templates._index.tsx #: apps/remix/app/components/tables/documents-table-empty-state.tsx msgid "We're all empty" @@ -6730,6 +7117,7 @@ msgstr "Bienvenue sur Documenso !" msgid "Were you trying to edit this document instead?" msgstr "Essayiez-vous d'éditer ce document à la place ?" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "When enabled, signers can choose who should sign next in the sequence instead of following the predefined order." @@ -6889,11 +7277,13 @@ msgstr "Vous ne pouvez pas avoir plus de {MAXIMUM_PASSKEYS} clés de passkey." msgid "You cannot modify a team member who has a higher role than you." msgstr "Vous ne pouvez pas modifier un membre de l'équipe qui a un rôle plus élevé que vous." +#: packages/ui/primitives/document-upload.tsx #: packages/ui/primitives/document-dropzone.tsx msgid "You cannot upload documents at this time." msgstr "Vous ne pouvez pas importer de documents pour le moment." #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "You cannot upload encrypted PDFs" msgstr "Vous ne pouvez pas importer de PDF cryptés" @@ -6915,7 +7305,7 @@ msgid "You have accepted an invitation from <0>{0} to join their team." msgstr "Vous avez accepté une invitation de <0>{0} pour rejoindre leur équipe." #. placeholder {0}: data.teamName -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "You have already completed the ownership transfer for <0>{0}." msgstr "Vous avez déjà terminé le transfert de propriété pour <0>{0}." @@ -6965,6 +7355,7 @@ msgstr "Vous avez initié le document {0} qui nécessite que vous {recipientActi msgid "You have no webhooks yet. Your webhooks will be shown here once you create them." msgstr "Vous n'avez pas encore de webhooks. Vos webhooks seront affichés ici une fois que vous les aurez créés." +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx #: apps/remix/app/routes/_authenticated+/templates._index.tsx msgid "You have not yet created any templates. To create a template please upload one." msgstr "Vous n'avez pas encore créé de modèles. Pour créer un modèle, veuillez en importer un." @@ -6979,6 +7370,7 @@ msgid "You have reached the maximum limit of {0} direct templates. <0>Upgrade yo msgstr "Vous avez atteint la limite maximale de {0} modèles directs. <0>Mettez à niveau votre compte pour continuer !" #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "You have reached your document limit for this month. Please upgrade your plan." msgstr "Vous avez atteint votre limite de documents pour ce mois. Veuillez passer à l'abonnement supérieur." @@ -7052,6 +7444,11 @@ msgstr "Vous devez avoir au moins un autre membre de l'équipe pour transférer msgid "You must set a profile URL before enabling your public profile." msgstr "Vous devez définir une URL de profil avant d'activer votre profil public." +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx +msgid "You must type '{deleteMessage}' to confirm" +msgstr "" + #: apps/remix/app/routes/_authenticated+/settings+/tokens.tsx msgid "You need to be an admin to manage API tokens." msgstr "Vous devez être administrateur pour gérer les tokens API." @@ -7117,6 +7514,8 @@ msgid "Your direct signing templates" msgstr "Vos modèles de signature directe" #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx msgid "Your document failed to upload." msgstr "L'importation de votre document a échoué." @@ -7124,6 +7523,10 @@ msgstr "L'importation de votre document a échoué." msgid "Your document has been created from the template successfully." msgstr "Votre document a été créé à partir du modèle avec succès." +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Your document has been created successfully" +msgstr "" + #: packages/email/template-components/template-document-super-delete.tsx msgid "Your document has been deleted by an admin!" msgstr "Votre document a été supprimé par un administrateur !" @@ -7141,6 +7544,7 @@ msgid "Your document has been successfully duplicated." msgstr "Votre document a été dupliqué avec succès." #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "Your document has been uploaded successfully." msgstr "Votre document a été importé avec succès." @@ -7234,6 +7638,10 @@ msgstr "Votre équipe a été supprimée avec succès." msgid "Your team has been successfully updated." msgstr "Votre équipe a été mise à jour avec succès." +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Your template has been created successfully" +msgstr "" + #: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx msgid "Your template has been duplicated successfully." msgstr "Votre modèle a été dupliqué avec succès." @@ -7261,4 +7669,3 @@ msgstr "Votre token a été créé avec succès ! Assurez-vous de le copier car #: apps/remix/app/routes/_authenticated+/settings+/tokens.tsx msgid "Your tokens will be shown here once you create them." msgstr "Vos tokens seront affichés ici une fois que vous les aurez créés." - diff --git a/packages/lib/translations/it/web.po b/packages/lib/translations/it/web.po index 07711bcbc..3ce80437a 100644 --- a/packages/lib/translations/it/web.po +++ b/packages/lib/translations/it/web.po @@ -26,6 +26,10 @@ msgstr " Abilita la firma tramite link diretto" msgid " The events that will trigger a webhook to be sent to your URL." msgstr " Gli eventi che attiveranno un webhook da inviare al tuo URL." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid ".PDF documents accepted (max {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB)" +msgstr "" + #. placeholder {0}: team.name #: apps/remix/app/components/forms/team-document-preferences-form.tsx msgid "\"{0}\" has invited you to sign \"example document\"." @@ -96,11 +100,13 @@ msgid "{0, plural, one {1 matching field} other {# matching fields}}" msgstr "{0, plural, one {1 campo corrispondente} other {# campi corrispondenti}}" #. placeholder {0}: recipients.length +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.edit.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.edit.tsx msgid "{0, plural, one {1 Recipient} other {# Recipients}}" msgstr "{0, plural, one {1 destinatario} other {# destinatari}}" #. placeholder {0}: pendingRecipients.length +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}" msgstr "{0, plural, one {In attesa di 1 destinatario} other {In attesa di # destinatari}}" @@ -144,6 +150,7 @@ msgstr "{0} ha lasciato il team {teamName} su Documenso" #. placeholder {0}: remaining.documents #. placeholder {1}: quota.documents #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "{0} of {1} documents remaining this month." msgstr "{0} di {1} documenti rimanenti questo mese." @@ -161,6 +168,7 @@ msgid "{0} on behalf of \"{1}\" has invited you to {recipientActionVerb} the doc msgstr "{0} per conto di \"{1}\" ti ha invitato a {recipientActionVerb} il documento \"{2}\"." #. placeholder {0}: recipients.length +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "{0} Recipient(s)" msgstr "{0} Destinatario(i)" @@ -327,6 +335,10 @@ msgstr "{recipientActionVerb} documento" msgid "{recipientActionVerb} the document to complete the process." msgstr "{recipientActionVerb} il documento per completare il processo." +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "{recipientCount} recipients" +msgstr "" + #: packages/email/templates/document-created-from-direct-template.tsx msgid "{recipientName} {action} a document by using one of your direct links" msgstr "{recipientName} {action} un documento utilizzando uno dei tuoi link diretti" @@ -727,6 +739,7 @@ msgstr "Aggiungi" msgid "Add a document" msgstr "Aggiungi un documento" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Add a URL to redirect the user to once the document is signed" @@ -800,6 +813,7 @@ msgstr "Aggiungi un destinatario segnaposto" msgid "Add Placeholders" msgstr "Aggiungi segnaposto" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Add Signer" msgstr "Aggiungi un firmatario" @@ -808,6 +822,10 @@ msgstr "Aggiungi un firmatario" msgid "Add Signers" msgstr "Aggiungi firmatari" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +msgid "Add signers and configure signing preferences" +msgstr "" + #: apps/remix/app/components/dialogs/team-email-add-dialog.tsx msgid "Add team email" msgstr "Aggiungi email del team" @@ -853,11 +871,16 @@ msgstr "Pannello admin" msgid "Advanced Options" msgstr "Opzioni avanzate" +#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Advanced settings" msgstr "Impostazioni avanzate" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Advanced Settings" +msgstr "" + #: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx msgid "After signing a document electronically, you will be provided the opportunity to view, download, and print the document for your records. It is highly recommended that you retain a copy of all electronically signed documents for your personal records. We will also retain a copy of the signed document for our records however we may not be able to provide you with a copy of the signed document after a certain period of time." msgstr "Dopo aver firmato un documento elettronicamente, avrai la possibilità di visualizzare, scaricare e stampare il documento per i tuoi archivi. È altamente consigliato conservare una copia di tutti i documenti firmati elettronicamente per i tuoi archivi personali. Noi conserveremo anche una copia del documento firmato per i nostri archivi, tuttavia potremmo non essere in grado di fornirti una copia del documento firmato dopo un certo periodo di tempo." @@ -882,6 +905,11 @@ msgstr "Tutti i documenti sono stati elaborati. Eventuali nuovi documenti inviat msgid "All documents related to the electronic signing process will be provided to you electronically through our platform or via email. It is your responsibility to ensure that your email address is current and that you can receive and open our emails." msgstr "Tutti i documenti relativi al processo di firma elettronica ti saranno forniti elettronicamente tramite la nostra piattaforma o via email. È tua responsabilità assicurarti che il tuo indirizzo email sia attuale e che tu possa ricevere e aprire le nostre email." +#: apps/remix/app/routes/_authenticated+/templates.folders._index.tsx +#: apps/remix/app/routes/_authenticated+/documents.folders._index.tsx +msgid "All Folders" +msgstr "" + #: apps/remix/app/components/dialogs/document-delete-dialog.tsx msgid "All inserted signatures will be voided" msgstr "Tutte le firme inserite saranno annullate" @@ -910,11 +938,13 @@ msgstr "Tutto il tempo" msgid "Allow document recipients to reply directly to this email address" msgstr "Consenti ai destinatari del documento di rispondere direttamente a questo indirizzo email" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Allow signers to dictate next signer" msgstr "Permetti ai firmatari di scegliere il prossimo firmatario" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Allowed Signature Types" @@ -998,8 +1028,11 @@ msgstr "Si è verificato un errore durante la disabilitazione della firma tramit msgid "An error occurred while disabling the user." msgstr "Si è verificato un errore durante la disabilitazione dell'utente." +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx +#: apps/remix/app/components/general/share-document-download-button.tsx +#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx msgid "An error occurred while downloading your document." @@ -1025,10 +1058,12 @@ msgstr "Si è verificato un errore durante il caricamento dei membri del team. P msgid "An error occurred while loading the document." msgstr "Si è verificato un errore durante il caricamento del documento." +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "An error occurred while moving the document." msgstr "Si è verificato un errore durante lo spostamento del documento." +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "An error occurred while moving the template." msgstr "Si è verificato un errore durante lo spostamento del modello." @@ -1103,6 +1138,7 @@ msgid "An error occurred while updating your profile." msgstr "Si è verificato un errore durante l'aggiornamento del tuo profilo." #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "An error occurred while uploading your document." msgstr "Si è verificato un errore durante il caricamento del tuo documento." @@ -1140,6 +1176,21 @@ msgstr "Si è verificato un errore inaspettato." msgid "An unknown error occurred" msgstr "Si è verificato un errore sconosciuto" +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +#: apps/remix/app/components/dialogs/folder-create-dialog.tsx +msgid "An unknown error occurred while creating the folder." +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx +msgid "An unknown error occurred while deleting the folder." +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx +#: apps/remix/app/components/dialogs/folder-move-dialog.tsx +msgid "An unknown error occurred while moving the folder." +msgstr "" + #: apps/remix/app/components/dialogs/team-transfer-dialog.tsx msgid "Any payment methods attached to this team will remain attached to this team. Please contact us if you need to update this information." msgstr "Qualsiasi metodo di pagamento associato a questo team rimarrà associato a questo team. Si prega di contattarci se necessario per aggiornare queste informazioni." @@ -1297,6 +1348,8 @@ msgstr "In attesa della conferma email" #: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx #: apps/remix/app/components/forms/signup.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx msgid "Back" msgstr "Indietro" @@ -1447,6 +1500,7 @@ msgstr "Può preparare" #: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx #: apps/remix/app/components/dialogs/webhook-create-dialog.tsx #: apps/remix/app/components/dialogs/token-delete-dialog.tsx +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx #: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx @@ -1466,6 +1520,7 @@ msgstr "Può preparare" #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx #: apps/remix/app/components/dialogs/passkey-create-dialog.tsx #: apps/remix/app/components/dialogs/document-resend-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx #: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx #: apps/remix/app/components/dialogs/document-delete-dialog.tsx @@ -1480,6 +1535,10 @@ msgstr "Annulla" msgid "Cancelled by user" msgstr "Annullato dall'utente" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Cannot remove document" +msgstr "" + #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Cannot remove signer" msgstr "Impossibile rimuovere il firmatario" @@ -1533,6 +1592,10 @@ msgstr "Scegli Destinatario Link Diretto" msgid "Choose how the document will reach recipients" msgstr "Scegli come il documento verrà inviato ai destinatari" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Choose how to distribute your document to recipients. Email will send notifications, None will generate signing links for manual distribution." +msgstr "" + #: apps/remix/app/components/forms/token.tsx msgid "Choose..." msgstr "Scegli..." @@ -1602,6 +1665,10 @@ msgstr "Clicca per inserire il campo" msgid "Close" msgstr "Chiudi" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Communication" +msgstr "" + #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/forms/signup.tsx @@ -1658,10 +1725,33 @@ msgstr "Documenti Completati" msgid "Completed Documents" msgstr "Documenti Completati" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "Completed on {formattedDate}" +msgstr "" + +#. placeholder {0}: parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[currentField.type]) +#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx +msgid "Configure {0} Field" +msgstr "" + +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Configure additional options and preferences" +msgstr "" + #: packages/lib/constants/template.ts msgid "Configure Direct Recipient" msgstr "Configura destinatario diretto" +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Configure Document" +msgstr "" + +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +msgid "Configure Fields" +msgstr "" + #: apps/remix/app/components/general/document/document-edit-form.tsx msgid "Configure general settings for the document." msgstr "Configura le impostazioni generali per il documento." @@ -1674,12 +1764,22 @@ msgstr "Configura le impostazioni generali per il modello." msgid "Configure template" msgstr "Configura il modello" +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Configure Template" +msgstr "" + #. placeholder {0}: parseMessageDescriptor( _, FRIENDLY_FIELD_TYPE[currentField.type], ) +#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Configure the {0} field" msgstr "Configura il campo {0}" +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +msgid "Configure the fields you want to place on the document." +msgstr "" + #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx msgid "Confirm" @@ -1695,6 +1795,8 @@ msgstr "Conferma digitando <0>{deleteMessage}" #: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx #: apps/remix/app/components/dialogs/token-delete-dialog.tsx +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx msgid "Confirm by typing: <0>{deleteMessage}" msgstr "Conferma digitando: <0>{deleteMessage}" @@ -1724,12 +1826,13 @@ msgid "Content" msgstr "Contenuto" #: apps/remix/app/routes/_unauthenticated+/verify-email.$token.tsx -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx #: apps/remix/app/components/general/document-signing/document-signing-form.tsx +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx #: apps/remix/app/components/dialogs/passkey-create-dialog.tsx #: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx @@ -1940,6 +2043,7 @@ msgstr "Creato" msgid "Created At" msgstr "Creato il" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Created by" msgstr "Creato da" @@ -1989,10 +2093,12 @@ msgstr "Modalità Scura" msgid "Date" msgstr "Data" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Date created" msgstr "Data di creazione" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Date Format" @@ -2042,8 +2148,11 @@ msgstr "Elimina" #. placeholder {0}: webhook.webhookUrl #. placeholder {0}: token.name +#. placeholder {0}: folder?.name ?? 'folder' #: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx #: apps/remix/app/components/dialogs/token-delete-dialog.tsx +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx msgid "delete {0}" msgstr "elimina {0}" @@ -2217,6 +2326,10 @@ msgstr "Mostra il tuo nome e email nei documenti" msgid "Distribute Document" msgstr "Distribuire il documento" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Distribution Method" +msgstr "" + #: apps/remix/app/components/dialogs/template-delete-dialog.tsx msgid "Do you want to delete this template?" msgstr "Vuoi eliminare questo modello?" @@ -2229,6 +2342,7 @@ msgstr "Vuoi duplicare questo modello?" msgid "Documenso will delete <0>all of your documents, along with all of your completed documents, signatures, and all other resources belonging to your Account." msgstr "Documenso eliminerà <0>tutti i tuoi documenti, insieme a tutti i tuoi documenti completati, firme e tutte le altre risorse appartenenti al tuo account." +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx msgid "Document" @@ -2294,6 +2408,10 @@ msgstr "Documento completato!" msgid "Document created" msgstr "Documento creato" +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Document Created" +msgstr "" + #. placeholder {0}: document.user.name #: apps/remix/app/components/general/template/template-page-view-recent-activity.tsx msgid "Document created by <0>{0}" @@ -2312,6 +2430,7 @@ msgstr "Documento creato usando un <0>link diretto" msgid "Document Creation" msgstr "Creazione del documento" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx #: apps/remix/app/components/dialogs/document-delete-dialog.tsx #: apps/remix/app/components/dialogs/admin-document-delete-dialog.tsx @@ -2344,12 +2463,18 @@ msgstr "Documento Duplicato" msgid "Document external ID updated" msgstr "ID esterno del documento aggiornato" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "Document found in your account" +msgstr "" + +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx #: apps/remix/app/components/general/document/document-history-sheet.tsx msgid "Document history" msgstr "Cronologia del documento" #: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Document ID" msgstr "ID del documento" @@ -2358,6 +2483,14 @@ msgstr "ID del documento" msgid "Document inbox" msgstr "Posta in arrivo del documento" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Document is already uploaded" +msgstr "" + +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Document is using legacy field insertion" +msgstr "" + #: apps/remix/app/components/tables/templates-table.tsx msgid "Document Limit Exceeded!" msgstr "Limite di documenti superato!" @@ -2366,6 +2499,7 @@ msgstr "Limite di documenti superato!" msgid "Document metrics" msgstr "Metriche del documento" +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Document moved" msgstr "Documento spostato" @@ -2429,10 +2563,12 @@ msgstr "Autenticazione firma documento aggiornata" msgid "Document signing process will be cancelled" msgstr "Il processo di firma del documento sarà annullato" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Document status" msgstr "Stato del documento" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Document title" msgstr "Titolo del documento" @@ -2445,11 +2581,16 @@ msgstr "Titolo documento aggiornato" msgid "Document updated" msgstr "Documento aggiornato" +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +msgid "Document updated successfully" +msgstr "" + #: apps/remix/app/components/general/document/document-upload.tsx msgid "Document upload disabled due to unpaid invoices" msgstr "Caricamento del documento disabilitato a causa di fatture non pagate" #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "Document uploaded" msgstr "Documento caricato" @@ -2466,6 +2607,9 @@ msgid "Document will be permanently deleted" msgstr "Il documento sarà eliminato definitivamente" #: apps/remix/app/routes/_profile+/p.$url.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId._index.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.edit.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.edit.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx @@ -2480,6 +2624,7 @@ msgstr "Il documento sarà eliminato definitivamente" msgid "Documents" msgstr "Documenti" +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Documents created from template" msgstr "Documenti creati da modello" @@ -2500,6 +2645,7 @@ msgstr "Non hai un account? <0>Registrati" #: apps/remix/app/components/tables/team-settings-billing-invoices-table.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx +#: apps/remix/app/components/general/share-document-download-button.tsx #: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx #: apps/remix/app/components/forms/2fa/view-recovery-codes-dialog.tsx @@ -2517,6 +2663,11 @@ msgstr "Scarica i log di audit" msgid "Download Certificate" msgstr "Scarica il certificato" +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx +#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx +msgid "Download Original" +msgstr "" + #: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx msgid "Download Template CSV" msgstr "Scarica Modello CSV" @@ -2539,10 +2690,22 @@ msgstr "Documenti redatti" msgid "Drag & drop your PDF here." msgstr "Trascina e rilascia il tuo PDF qui." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Drag and drop or click to upload" +msgstr "" + +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +msgid "Drag and drop your PDF file here" +msgstr "" + #: packages/lib/constants/document.ts msgid "Draw" msgstr "Disegno" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Drop your document here" +msgstr "" + #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Dropdown" @@ -2577,6 +2740,7 @@ msgstr "Duplica" msgid "Edit" msgstr "Modifica" +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Edit Template" msgstr "Modifica Modello" @@ -2607,6 +2771,9 @@ msgstr "Divulgazione della firma elettronica" #: apps/remix/app/components/forms/forgot-password.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx @@ -2700,6 +2867,7 @@ msgstr "Abilita il branding personalizzato per tutti i documenti in questo team. msgid "Enable Direct Link Signing" msgstr "Abilita la firma di link diretto" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Enable signing order" @@ -2746,6 +2914,10 @@ msgstr "Inserisci il tuo nome" msgid "Enter your text here" msgstr "Inserisci il tuo testo qui" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx #: apps/remix/app/components/general/verify-email-banner.tsx @@ -2780,10 +2952,15 @@ msgstr "Inserisci il tuo testo qui" #: apps/remix/app/components/general/document/document-edit-form.tsx #: apps/remix/app/components/general/document/document-edit-form.tsx #: apps/remix/app/components/general/document/document-edit-form.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx #: apps/remix/app/components/dialogs/webhook-create-dialog.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx #: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx #: apps/remix/app/components/dialogs/admin-user-enable-dialog.tsx #: apps/remix/app/components/dialogs/admin-user-disable-dialog.tsx @@ -2793,6 +2970,10 @@ msgstr "Inserisci il tuo testo qui" msgid "Error" msgstr "Errore" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Error uploading file" +msgstr "" + #: apps/remix/app/components/forms/team-document-preferences-form.tsx msgid "Everyone can access and view the document" msgstr "Tutti possono accedere e visualizzare il documento" @@ -2823,6 +3004,11 @@ msgstr "Scade il {0}" msgid "External ID" msgstr "ID esterno" +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +msgid "Failed to create folder" +msgstr "" + #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx msgid "Failed to reseal document" msgstr "Fallito il risigillo del documento" @@ -2831,10 +3017,18 @@ msgstr "Fallito il risigillo del documento" msgid "Failed to save settings." msgstr "Impossibile salvare le impostazioni." +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +msgid "Failed to update document" +msgstr "" + #: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx msgid "Failed to update recipient" msgstr "Aggiornamento destinario fallito" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +msgid "Failed to update template" +msgstr "" + #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id.tsx #: apps/remix/app/routes/_authenticated+/settings+/webhooks.$id.tsx msgid "Failed to update webhook" @@ -2887,7 +3081,13 @@ msgstr "Campo non firmato" msgid "Fields" msgstr "Campi" +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Fields updated" +msgstr "" + #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx msgid "File cannot be larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB" msgstr "Il file non può essere più grande di {APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB" @@ -2895,6 +3095,25 @@ msgstr "Il file non può essere più grande di {APP_DOCUMENT_UPLOAD_SIZE_LIMIT} msgid "File size exceeds the limit of {APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB" msgstr "La dimensione del file supera il limite di {APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Folder" +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +msgid "Folder created successfully" +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx +#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx +msgid "Folder not found" +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx +#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx +msgid "Folder updated successfully" +msgstr "" + #: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx #: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx #: packages/ui/primitives/document-flow/field-items-advanced-settings/name-field.tsx @@ -2941,6 +3160,7 @@ msgstr "Nome completo" #: apps/remix/app/components/general/teams/team-settings-nav-desktop.tsx #: apps/remix/app/components/general/document/document-edit-form.tsx #: apps/remix/app/components/general/direct-template/direct-template-page.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx msgid "General" msgstr "Generale" @@ -2971,6 +3191,10 @@ msgstr "Torna alla home" msgid "Go Back Home" msgstr "Torna alla home" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "Go to document" +msgstr "" + #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx msgid "Go to owner" msgstr "Vai al proprietario" @@ -3142,7 +3366,7 @@ msgstr "Codice non valido. Riprova." msgid "Invalid email" msgstr "Email non valida" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx msgid "Invalid link" msgstr "Link non valido" @@ -3239,6 +3463,7 @@ msgid "Label" msgstr "Etichetta" #: apps/remix/app/components/general/menu-switcher.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Language" @@ -3261,6 +3486,7 @@ msgstr "Ultimi 7 giorni" msgid "Last modified" msgstr "Ultima modifica" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Last updated" msgstr "Ultimo aggiornamento" @@ -3363,6 +3589,7 @@ msgstr "Gestisci il profilo di {0}" msgid "Manage all teams you are currently associated with." msgstr "Gestisci tutti i team a cui sei attualmente associato." +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Manage and view template" msgstr "Gestisci e visualizza il modello" @@ -3471,6 +3698,7 @@ msgstr "Membro dal" msgid "Members" msgstr "Membri" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-subject.tsx msgid "Message <0>(Optional)" @@ -3498,19 +3726,38 @@ msgstr "Utenti attivi mensili: Utenti che hanno creato almeno un documento" msgid "Monthly Active Users: Users that had at least one of their documents completed" msgstr "Utenti attivi mensili: Utenti con almeno uno dei loro documenti completati" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Move" msgstr "Sposta" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "Move \"{templateTitle}\" to a folder" +msgstr "" + +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Move Document to Folder" +msgstr "" + #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Move Document to Team" msgstr "Sposta documento al team" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "Move Template to Folder" +msgstr "" + #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "Move Template to Team" msgstr "Sposta modello al team" +#: apps/remix/app/components/tables/templates-table-action-dropdown.tsx +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx +msgid "Move to Folder" +msgstr "" + #: apps/remix/app/components/tables/templates-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx msgid "Move to Team" @@ -3534,6 +3781,8 @@ msgstr "I miei modelli" #: apps/remix/app/components/general/claim-account.tsx #: apps/remix/app/components/general/document-signing/document-signing-name-field.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx @@ -3622,8 +3871,8 @@ msgstr "Nessuna attività recente" msgid "No recent documents" msgstr "Nessun documento recente" +#: packages/ui/primitives/recipient-selector.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx -#: packages/ui/primitives/document-flow/add-fields.tsx msgid "No recipient matching this description was found." msgstr "Nessun destinatario corrispondente a questa descrizione è stato trovato." @@ -3634,8 +3883,8 @@ msgstr "Nessun destinatario corrispondente a questa descrizione è stato trovato msgid "No recipients" msgstr "Nessun destinatario" +#: packages/ui/primitives/recipient-selector.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx -#: packages/ui/primitives/document-flow/add-fields.tsx msgid "No recipients with this role" msgstr "Nessun destinatario con questo ruolo" @@ -3677,6 +3926,7 @@ msgstr "Nessun valore trovato." msgid "No worries, it happens! Enter your email and we'll email you a special link to reset your password." msgstr "Non ti preoccupare, succede! Inserisci la tua email e ti invieremo un link speciale per reimpostare la tua password." +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/lib/constants/document.ts msgid "None" msgstr "Nessuno" @@ -3900,8 +4150,8 @@ msgstr "Pagamento scaduto" #: apps/remix/app/components/tables/user-settings-teams-page-table.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx #: apps/remix/app/components/general/document/document-status.tsx -#: apps/remix/app/components/general/document/document-read-only-fields.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx +#: packages/ui/components/document/document-read-only-fields.tsx #: packages/lib/constants/document.ts msgid "Pending" msgstr "In sospeso" @@ -3989,6 +4239,11 @@ msgstr "Per favore controlla la tua email per aggiornamenti." msgid "Please choose your new password" msgstr "Per favore scegli la tua nuova password" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +msgid "Please configure the document first" +msgstr "" + #: packages/lib/server-only/auth/send-confirmation-email.ts msgid "Please confirm your email" msgstr "Per favore conferma la tua email" @@ -4261,6 +4516,7 @@ msgstr "Destinatario aggiornato" #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx #: apps/remix/app/components/general/template/template-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx msgid "Recipients" msgstr "Destinatari" @@ -4284,6 +4540,7 @@ msgstr "Codici di recupero" msgid "Red" msgstr "Rosso" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Redirect URL" @@ -4345,7 +4602,6 @@ msgstr "Promemoria: per favore {recipientActionVerb} il tuo documento" #: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx #: apps/remix/app/components/general/teams/team-email-dropdown.tsx #: apps/remix/app/components/general/document-signing/document-signing-field-container.tsx -#: apps/remix/app/components/general/document-signing/document-signing-field-container.tsx #: apps/remix/app/components/forms/team-branding-preferences-form.tsx #: apps/remix/app/components/forms/avatar-image.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx @@ -4442,7 +4698,7 @@ msgstr "Conservazione dei documenti" msgid "Retry" msgstr "Riprova" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.decline.$token.tsx @@ -4473,6 +4729,7 @@ msgstr "Revoca l'accesso" #: apps/remix/app/components/tables/user-settings-current-teams-table.tsx #: apps/remix/app/components/tables/team-settings-members-table.tsx #: apps/remix/app/components/tables/team-settings-member-invites-table.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx #: apps/remix/app/components/dialogs/team-member-invite-dialog.tsx @@ -4484,12 +4741,19 @@ msgstr "Ruolo" msgid "Roles" msgstr "Ruoli" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Root (No Folder)" +msgstr "" + #: packages/ui/primitives/data-table-pagination.tsx msgid "Rows per page" msgstr "Righe per pagina" #: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx #: apps/remix/app/components/general/document-signing/document-signing-number-field.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx msgid "Save" @@ -4544,6 +4808,10 @@ msgstr "Attività di sicurezza" msgid "Select" msgstr "Seleziona" +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Select a folder to move this document to." +msgstr "" + #: apps/remix/app/components/dialogs/template-move-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Select a team" @@ -4665,6 +4933,14 @@ msgstr "Inviato" msgid "Set a password" msgstr "Imposta una password" +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Set up your document properties and recipient information" +msgstr "" + +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Set up your template properties and recipient information" +msgstr "" + #: apps/remix/app/routes/_authenticated+/settings+/_layout.tsx #: apps/remix/app/components/general/app-nav-mobile.tsx #: apps/remix/app/components/general/app-command-menu.tsx @@ -4806,7 +5082,6 @@ msgstr "Iscriviti con OIDC" #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/types.ts -#: packages/ui/primitives/document-flow/field-icon.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Signature" msgstr "Firma" @@ -4836,8 +5111,8 @@ msgid "Signatures will appear once the document has been completed" msgstr "Le firme appariranno una volta completato il documento" #: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx -#: apps/remix/app/components/general/document/document-read-only-fields.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx +#: packages/ui/components/document/document-read-only-fields.tsx #: packages/lib/constants/recipient-roles.ts msgid "Signed" msgstr "Firmato" @@ -4934,7 +5209,9 @@ msgstr "Alcuni firmatari non hanno un campo firma assegnato. Assegna almeno 1 ca #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx +#: apps/remix/app/components/general/share-document-download-button.tsx #: apps/remix/app/components/general/billing-portal-button.tsx #: apps/remix/app/components/general/billing-plans.tsx #: apps/remix/app/components/general/teams/team-layout-billing-banner.tsx @@ -4946,6 +5223,7 @@ msgstr "Alcuni firmatari non hanno un campo firma assegnato. Assegna almeno 1 ca #: apps/remix/app/components/general/document-signing/document-signing-auth-page.tsx #: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx #: apps/remix/app/components/general/document/document-page-view-dropdown.tsx +#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx #: apps/remix/app/components/general/document/document-certificate-download-button.tsx #: apps/remix/app/components/general/document/document-audit-log-download-button.tsx @@ -4972,7 +5250,7 @@ msgid "Something went wrong" msgstr "Qualcosa è andato storto" #. placeholder {0}: data.teamName -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Something went wrong while attempting to transfer the ownership of team <0>{0} to your. Please try again later or contact support." msgstr "Qualcosa è andato storto durante il tentativo di trasferimento della proprietà del team <0>{0} a te. Riprova più tardi o contatta il supporto." @@ -5044,6 +5322,7 @@ msgstr "Stato" msgid "Step <0>{step} of {maxStep}" msgstr "Passo <0>{step} di {maxStep}" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-subject.tsx msgid "Subject <0>(Optional)" @@ -5061,6 +5340,8 @@ msgstr "Abbonamento" msgid "Subscriptions" msgstr "Abbonamenti" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx #: apps/remix/app/components/tables/user-settings-pending-teams-table-actions.tsx #: apps/remix/app/components/tables/team-settings-member-invites-table.tsx @@ -5201,15 +5482,15 @@ msgstr "Solo team" msgid "Team only templates are not linked anywhere and are visible only to your team." msgstr "I modelli solo per il team non sono collegati da nessuna parte e sono visibili solo al tuo team." -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Team ownership transfer" msgstr "Trasferimento di proprietà del team" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Team ownership transfer already completed!" msgstr "Trasferimento della proprietà del team già completato!" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Team ownership transferred!" msgstr "Proprietà del team trasferita!" @@ -5258,6 +5539,8 @@ msgstr "Team" msgid "Teams restricted" msgstr "Team limitati" +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id.edit.tsx +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id.edit.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx @@ -5268,6 +5551,10 @@ msgstr "Team limitati" msgid "Template" msgstr "Modello" +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Template Created" +msgstr "" + #: apps/remix/app/components/dialogs/template-delete-dialog.tsx msgid "Template deleted" msgstr "Modello eliminato" @@ -5288,6 +5575,11 @@ msgstr "Il modello è stato rimosso dal tuo profilo pubblico." msgid "Template has been updated." msgstr "Il modello è stato aggiornato." +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Template is using legacy field insertion" +msgstr "" + +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "Template moved" msgstr "Modello spostato" @@ -5304,6 +5596,12 @@ msgstr "Modello salvato" msgid "Template title" msgstr "Titolo del modello" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +msgid "Template updated successfully" +msgstr "" + +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx #: apps/remix/app/components/general/app-nav-mobile.tsx @@ -5379,10 +5677,18 @@ msgstr "Il contenuto da mostrare nel banner, HTML è consentito" msgid "The direct link has been copied to your clipboard" msgstr "Il link diretto è stato copiato negli appunti" +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "The document has been moved successfully." +msgstr "" + #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "The document has been successfully moved to the selected team." msgstr "Il documento è stato spostato con successo al team selezionato." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "The document is already saved and cannot be changed." +msgstr "" + #: apps/remix/app/components/embed/embed-document-completed.tsx msgid "The document is now completed, please follow any instructions provided within the parent application." msgstr "Il documento è ora completato, si prega di seguire eventuali istruzioni fornite nell'applicazione principale." @@ -5421,6 +5727,28 @@ msgstr "L'email o la password fornita è errata" msgid "The events that will trigger a webhook to be sent to your URL." msgstr "Gli eventi che scateneranno un webhook da inviare al tuo URL." +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "The fields have been updated to the new field insertion method successfully" +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx +msgid "The folder you are trying to delete does not exist." +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx +#: apps/remix/app/components/dialogs/folder-move-dialog.tsx +msgid "The folder you are trying to move does not exist." +msgstr "" + +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "The folder you are trying to move the document to does not exist." +msgstr "" + +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "The folder you are trying to move the template to does not exist." +msgstr "" + #: packages/email/templates/bulk-send-complete.tsx msgid "The following errors occurred:" msgstr "Si sono verificati i seguenti errori:" @@ -5434,7 +5762,7 @@ msgid "The following team has been deleted by you" msgstr "Il seguente team è stato eliminato da te" #. placeholder {0}: data.teamName -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "The ownership of team <0>{0} has been successfully transferred to you." msgstr "La proprietà del team <0>{0} è stata trasferita con successo a te." @@ -5531,11 +5859,17 @@ msgid "The team transfer request to <0>{0} has expired." msgstr "La richiesta di trasferimento del team a <0>{0} è scaduta." #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/_layout.tsx -msgid "The team you are looking for may have been removed, renamed or may have never\n" +msgid "" +"The team you are looking for may have been removed, renamed or may have never\n" " existed." -msgstr "La squadra che stai cercando potrebbe essere stata rimossa, rinominata o potrebbe non essere mai\n" +msgstr "" +"La squadra che stai cercando potrebbe essere stata rimossa, rinominata o potrebbe non essere mai\n" " esistita." +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "The template has been moved successfully." +msgstr "" + #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "The template has been successfully moved to the selected team." msgstr "Il modello è stato spostato con successo al team selezionato." @@ -5591,6 +5925,10 @@ msgstr "Non ci sono bozze attive al momento attuale. Puoi caricare un documento msgid "There are no completed documents yet. Documents that you have created or received will appear here once completed." msgstr "Non ci sono ancora documenti completati. I documenti che hai creato o ricevuto appariranno qui una volta completati." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "There was an error uploading your file. Please try again." +msgstr "" + #: apps/remix/app/components/general/teams/team-email-usage.tsx msgid "They have permission on your behalf to:" msgstr "Hanno il permesso per tuo conto di:" @@ -5621,6 +5959,10 @@ msgstr "Questo può essere sovrascritto impostando i requisiti di autenticazione msgid "This document can not be recovered, if you would like to dispute the reason for future documents please contact support." msgstr "Questo documento non può essere recuperato, se vuoi contestare la ragione per i documenti futuri, contatta il supporto." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "This document cannot be changed" +msgstr "" + #: apps/remix/app/components/dialogs/document-delete-dialog.tsx msgid "This document could not be deleted at this time. Please try again." msgstr "Questo documento non può essere eliminato in questo momento. Riprova." @@ -5633,7 +5975,7 @@ msgstr "Questo documento non può essere duplicato in questo momento. Riprova." msgid "This document could not be re-sent at this time. Please try again." msgstr "Questo documento non può essere rinviato in questo momento. Riprova." -#: packages/ui/primitives/document-flow/add-fields.tsx +#: packages/ui/primitives/recipient-selector.tsx msgid "This document has already been sent to this recipient. You can no longer edit this recipient." msgstr "Questo documento è già stato inviato a questo destinatario. Non puoi più modificare questo destinatario." @@ -5645,18 +5987,29 @@ msgstr "Questo documento è stato annullato dal proprietario e non è più dispo msgid "This document has been cancelled by the owner." msgstr "Questo documento è stato annullato dal proprietario." +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "This document has been rejected by a recipient" msgstr "Questo documento è stato rifiutato da un destinatario" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "This document has been signed by all recipients" msgstr "Questo documento è stato firmato da tutti i destinatari" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "This document is available in your Documenso account. You can view more details, recipients, and audit logs there." +msgstr "" + +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "This document is currently a draft and has not been sent" msgstr "Questo documento è attualmente una bozza e non è stato inviato" +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "This document is using legacy field insertion, we recommend using the new field insertion method for more accurate results." +msgstr "" + #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx msgid "This document was created by you or a team member using the template above." msgstr "Questo documento è stato creato da te o un membro del team utilizzando il modello sopra." @@ -5697,11 +6050,16 @@ msgstr "Questa email sarà inviata al destinatario che ha appena firmato il docu msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them." msgstr "Questo campo non può essere modificato o eliminato. Quando condividi il link diretto di questo modello o lo aggiungi al tuo profilo pubblico, chiunque vi acceda può inserire il proprio nome e email, e compilare i campi assegnati." +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +#: apps/remix/app/components/dialogs/folder-create-dialog.tsx +msgid "This folder name is already taken." +msgstr "" + #: packages/ui/primitives/template-flow/add-template-settings.tsx msgid "This is how the document will reach the recipients once the document is ready for signing." msgstr "È così che il documento raggiungerà i destinatari una volta pronto per essere firmato." -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "This link is invalid or has expired. Please contact your team to resend a transfer request." msgstr "Questo link è invalido o è scaduto. Si prega di contattare il tuo team per inviare nuovamente una richiesta di trasferimento." @@ -5741,6 +6099,10 @@ msgstr "Questo team e tutti i dati associati, escluse le fatture di fatturazione msgid "This template could not be deleted at this time. Please try again." msgstr "Questo modello non può essere eliminato in questo momento. Per favore prova di nuovo." +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "This template is using legacy field insertion, we recommend using the new field insertion method for more accurate results." +msgstr "" + #: apps/remix/app/routes/_unauthenticated+/team.decline.$token.tsx msgid "This token is invalid or has expired. No action is needed." msgstr "Questo token è invalido o è scaduto. Non è necessaria alcuna azione." @@ -5779,11 +6141,13 @@ msgstr "Questo sovrascriverà qualsiasi impostazione globale." msgid "Time" msgstr "Ora" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Time zone" msgstr "Fuso orario" #: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Time Zone" @@ -5793,6 +6157,7 @@ msgstr "Fuso orario" #: apps/remix/app/components/tables/templates-table.tsx #: apps/remix/app/components/tables/documents-table.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Title" msgstr "Titolo" @@ -6102,6 +6467,10 @@ msgstr "Aggiorna" msgid "Update Banner" msgstr "Aggiorna banner" +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Update Fields" +msgstr "" + #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx msgid "Update passkey" msgstr "Aggiorna chiave d'accesso" @@ -6116,6 +6485,7 @@ msgstr "Aggiorna profilo" #: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx +#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx msgid "Update Recipient" msgstr "Aggiorna destinatario" @@ -6158,10 +6528,15 @@ msgstr "Aggiornamento della password..." msgid "Updating Your Information" msgstr "Aggiornamento delle tue informazioni" +#: packages/ui/primitives/document-upload.tsx #: packages/ui/primitives/document-dropzone.tsx msgid "Upgrade" msgstr "Aggiorna" +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +msgid "Upgrade your plan to upload more documents" +msgstr "" + #: packages/lib/constants/document.ts msgid "Upload" msgstr "Carica" @@ -6190,10 +6565,17 @@ msgstr "Carica CSV" msgid "Upload custom document" msgstr "Carica documento personalizzato" +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +#: packages/ui/primitives/document-upload.tsx +msgid "Upload Document" +msgstr "" + #: packages/ui/primitives/signature-pad/signature-pad-upload.tsx msgid "Upload Signature" msgstr "Carica Firma" +#: packages/ui/primitives/document-upload.tsx #: packages/ui/primitives/document-dropzone.tsx msgid "Upload Template Document" msgstr "Carica Documento Modello" @@ -6219,6 +6601,11 @@ msgstr "Il file caricato è troppo piccolo" msgid "Uploaded file not an allowed file type" msgstr "Il file caricato non è di un tipo di file consentito" +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +msgid "Uploading document..." +msgstr "" + +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Use" msgstr "Utilizza" @@ -6667,6 +7054,7 @@ msgstr "Genereremo link di firma per te, che potrai inviare ai destinatari trami msgid "We won't send anything to notify recipients." msgstr "Non invieremo nulla per notificare i destinatari." +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx #: apps/remix/app/routes/_authenticated+/templates._index.tsx #: apps/remix/app/components/tables/documents-table-empty-state.tsx msgid "We're all empty" @@ -6731,6 +7119,7 @@ msgstr "Benvenuto su Documenso!" msgid "Were you trying to edit this document instead?" msgstr "Stavi provando a modificare questo documento invece?" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "When enabled, signers can choose who should sign next in the sequence instead of following the predefined order." @@ -6890,11 +7279,13 @@ msgstr "Non puoi avere più di {MAXIMUM_PASSKEYS} passkey." msgid "You cannot modify a team member who has a higher role than you." msgstr "Non puoi modificare un membro del team che ha un ruolo superiore al tuo." +#: packages/ui/primitives/document-upload.tsx #: packages/ui/primitives/document-dropzone.tsx msgid "You cannot upload documents at this time." msgstr "Non puoi caricare documenti in questo momento." #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "You cannot upload encrypted PDFs" msgstr "Non puoi caricare PDF crittografati" @@ -6916,7 +7307,7 @@ msgid "You have accepted an invitation from <0>{0} to join their team." msgstr "Hai accettato un invito da <0>{0} per unirti al loro team." #. placeholder {0}: data.teamName -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "You have already completed the ownership transfer for <0>{0}." msgstr "Hai già completato il trasferimento di proprietà per <0>{0}." @@ -6966,6 +7357,7 @@ msgstr "Hai avviato il documento {0} che richiede che tu lo {recipientActionVerb msgid "You have no webhooks yet. Your webhooks will be shown here once you create them." msgstr "Non hai ancora webhook. I tuoi webhook verranno visualizzati qui una volta creati." +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx #: apps/remix/app/routes/_authenticated+/templates._index.tsx msgid "You have not yet created any templates. To create a template please upload one." msgstr "Non hai ancora creato alcun modello. Per creare un modello, caricane uno." @@ -6980,6 +7372,7 @@ msgid "You have reached the maximum limit of {0} direct templates. <0>Upgrade yo msgstr "Hai raggiunto il limite massimo di {0} modelli diretti. <0>Aggiorna il tuo account per continuare!" #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "You have reached your document limit for this month. Please upgrade your plan." msgstr "Hai raggiunto il limite dei documenti per questo mese. Si prega di aggiornare il proprio piano." @@ -7053,6 +7446,11 @@ msgstr "Devi avere almeno un altro membro del team per trasferire la proprietà. msgid "You must set a profile URL before enabling your public profile." msgstr "Devi impostare un'URL del profilo prima di abilitare il tuo profilo pubblico." +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx +msgid "You must type '{deleteMessage}' to confirm" +msgstr "" + #: apps/remix/app/routes/_authenticated+/settings+/tokens.tsx msgid "You need to be an admin to manage API tokens." msgstr "Devi essere un amministratore per gestire i token API." @@ -7118,6 +7516,8 @@ msgid "Your direct signing templates" msgstr "I tuoi modelli di firma diretta" #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx msgid "Your document failed to upload." msgstr "Il tuo documento non è stato caricato." @@ -7125,6 +7525,10 @@ msgstr "Il tuo documento non è stato caricato." msgid "Your document has been created from the template successfully." msgstr "Il tuo documento è stato creato con successo dal modello." +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Your document has been created successfully" +msgstr "" + #: packages/email/template-components/template-document-super-delete.tsx msgid "Your document has been deleted by an admin!" msgstr "Il tuo documento è stato eliminato da un amministratore!" @@ -7142,6 +7546,7 @@ msgid "Your document has been successfully duplicated." msgstr "Il tuo documento è stato duplicato correttamente." #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "Your document has been uploaded successfully." msgstr "Il tuo documento è stato caricato correttamente." @@ -7235,6 +7640,10 @@ msgstr "Il tuo team è stato eliminato correttamente." msgid "Your team has been successfully updated." msgstr "Il tuo team è stato aggiornato correttamente." +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Your template has been created successfully" +msgstr "" + #: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx msgid "Your template has been duplicated successfully." msgstr "Il tuo modello è stato duplicato correttamente." @@ -7262,4 +7671,3 @@ msgstr "Il tuo token è stato creato con successo! Assicurati di copiarlo perch #: apps/remix/app/routes/_authenticated+/settings+/tokens.tsx msgid "Your tokens will be shown here once you create them." msgstr "I tuoi token verranno mostrati qui una volta creati." - diff --git a/packages/lib/translations/pl/web.po b/packages/lib/translations/pl/web.po index 254e79c6b..592117374 100644 --- a/packages/lib/translations/pl/web.po +++ b/packages/lib/translations/pl/web.po @@ -26,6 +26,10 @@ msgstr " Włącz podpisywanie linku bezpośredniego" msgid " The events that will trigger a webhook to be sent to your URL." msgstr " Zdarzenia, które uruchomią wysłanie webhooka na Twój URL." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid ".PDF documents accepted (max {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB)" +msgstr "" + #. placeholder {0}: team.name #: apps/remix/app/components/forms/team-document-preferences-form.tsx msgid "\"{0}\" has invited you to sign \"example document\"." @@ -96,11 +100,13 @@ msgid "{0, plural, one {1 matching field} other {# matching fields}}" msgstr "{0, plural, one {1 pasujące pole} few {# pasujące pola} many {# pasujących pól} other {# pasujących pól}}" #. placeholder {0}: recipients.length +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.edit.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.edit.tsx msgid "{0, plural, one {1 Recipient} other {# Recipients}}" msgstr "{0, plural, one {1 odbiorca} few {# odbiorców} many {# odbiorców} other {# odbiorców}}" #. placeholder {0}: pendingRecipients.length +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}" msgstr "{0, plural, one {Czekam na 1 odbiorcę} few {Czekam na # odbiorców} many {Czekam na # odbiorców} other {Czekam na # odbiorców}}" @@ -144,6 +150,7 @@ msgstr "{0} opuścił zespół {teamName} na Documenso" #. placeholder {0}: remaining.documents #. placeholder {1}: quota.documents #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "{0} of {1} documents remaining this month." msgstr "{0} z {1} dokumentów pozostałych w tym miesiącu." @@ -161,6 +168,7 @@ msgid "{0} on behalf of \"{1}\" has invited you to {recipientActionVerb} the doc msgstr "{0} w imieniu \"{1}\" zaprosił Cię do {recipientActionVerb} dokument „{2}”." #. placeholder {0}: recipients.length +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "{0} Recipient(s)" msgstr "{0} Odbiorca(ów)" @@ -327,6 +335,10 @@ msgstr "{recipientActionVerb} dokument" msgid "{recipientActionVerb} the document to complete the process." msgstr "{recipientActionVerb} dokument, aby zakończyć proces." +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "{recipientCount} recipients" +msgstr "" + #: packages/email/templates/document-created-from-direct-template.tsx msgid "{recipientName} {action} a document by using one of your direct links" msgstr "{recipientName} {action} dokument, korzystając z jednego z Twoich bezpośrednich linków" @@ -727,6 +739,7 @@ msgstr "Dodaj" msgid "Add a document" msgstr "Dodaj dokument" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Add a URL to redirect the user to once the document is signed" @@ -800,6 +813,7 @@ msgstr "Dodaj odbiorcę zastępczego" msgid "Add Placeholders" msgstr "Dodaj znaczniki" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Add Signer" msgstr "Dodaj podpisującego" @@ -808,6 +822,10 @@ msgstr "Dodaj podpisującego" msgid "Add Signers" msgstr "Dodaj podpisujących" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +msgid "Add signers and configure signing preferences" +msgstr "" + #: apps/remix/app/components/dialogs/team-email-add-dialog.tsx msgid "Add team email" msgstr "Dodaj e-mail zespołowy" @@ -853,11 +871,16 @@ msgstr "Panel administratora" msgid "Advanced Options" msgstr "Opcje zaawansowane" +#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Advanced settings" msgstr "Ustawienia zaawansowane" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Advanced Settings" +msgstr "" + #: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx msgid "After signing a document electronically, you will be provided the opportunity to view, download, and print the document for your records. It is highly recommended that you retain a copy of all electronically signed documents for your personal records. We will also retain a copy of the signed document for our records however we may not be able to provide you with a copy of the signed document after a certain period of time." msgstr "Po podpisaniu dokumentu elektronicznie, otrzymasz możliwość obejrzenia, pobrania i wydrukowania dokumentu dla swoich zapisów. Zaleca się, abyś zachował kopię wszystkich podpisanych elektronicznie dokumentów dla swoich osobistych zapisów. My również zachowamy kopię podpisanego dokumentu w naszych zapisach, jednak możemy nie być w stanie dostarczyć ci kopii podpisanego dokumentu po pewnym czasie." @@ -882,6 +905,11 @@ msgstr "Wszystkie dokumenty zostały przetworzone. Nowe dokumenty, które zostan msgid "All documents related to the electronic signing process will be provided to you electronically through our platform or via email. It is your responsibility to ensure that your email address is current and that you can receive and open our emails." msgstr "Wszystkie dokumenty związane z procesem podpisywania elektronicznego będą dostarczane do Ciebie elektronicznie za pośrednictwem naszej platformy lub za pośrednictwem e-maila. To Twoja odpowiedzialność, aby upewnić się, że twój adres e-mail jest aktualny i że możesz odbierać i otwierać nasze e-maile." +#: apps/remix/app/routes/_authenticated+/templates.folders._index.tsx +#: apps/remix/app/routes/_authenticated+/documents.folders._index.tsx +msgid "All Folders" +msgstr "" + #: apps/remix/app/components/dialogs/document-delete-dialog.tsx msgid "All inserted signatures will be voided" msgstr "Wszystkie wstawione podpisy zostaną unieważnione" @@ -910,11 +938,13 @@ msgstr "Cały czas" msgid "Allow document recipients to reply directly to this email address" msgstr "Zezwól odbiorcom dokumentów na bezpośrednią odpowiedź na ten adres e-mail" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Allow signers to dictate next signer" msgstr "Pozwól sygnatariuszom wskazać następnego sygnatariusza" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Allowed Signature Types" @@ -998,8 +1028,11 @@ msgstr "Wystąpił błąd podczas dezaktywacji podpisywania za pomocą linku bez msgid "An error occurred while disabling the user." msgstr "Wystąpił błąd podczas wyłączania użytkownika." +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx +#: apps/remix/app/components/general/share-document-download-button.tsx +#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx msgid "An error occurred while downloading your document." @@ -1025,10 +1058,12 @@ msgstr "Wystąpił błąd podczas ładowania członków zespołu. Proszę sprób msgid "An error occurred while loading the document." msgstr "Wystąpił błąd podczas ładowania dokumentu." +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "An error occurred while moving the document." msgstr "Wystąpił błąd podczas przenoszenia dokumentu." +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "An error occurred while moving the template." msgstr "Wystąpił błąd podczas przenoszenia szablonu." @@ -1103,6 +1138,7 @@ msgid "An error occurred while updating your profile." msgstr "Wystąpił błąd podczas aktualizowania profilu." #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "An error occurred while uploading your document." msgstr "Wystąpił błąd podczas przesyłania dokumentu." @@ -1140,6 +1176,21 @@ msgstr "Wystąpił nieoczekiwany błąd." msgid "An unknown error occurred" msgstr "Wystąpił nieznany błąd" +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +#: apps/remix/app/components/dialogs/folder-create-dialog.tsx +msgid "An unknown error occurred while creating the folder." +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx +msgid "An unknown error occurred while deleting the folder." +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx +#: apps/remix/app/components/dialogs/folder-move-dialog.tsx +msgid "An unknown error occurred while moving the folder." +msgstr "" + #: apps/remix/app/components/dialogs/team-transfer-dialog.tsx msgid "Any payment methods attached to this team will remain attached to this team. Please contact us if you need to update this information." msgstr "Jakiekolwiek metody płatności przypisane do tego zespołu pozostaną przypisane do tego zespołu. Proszę skontaktować się z nami, jeśli potrzebujesz zaktualizować te informacje." @@ -1297,6 +1348,8 @@ msgstr "Czekam na potwierdzenie e-maila" #: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx #: apps/remix/app/components/forms/signup.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx msgid "Back" msgstr "Powrót" @@ -1447,6 +1500,7 @@ msgstr "Może przygotować" #: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx #: apps/remix/app/components/dialogs/webhook-create-dialog.tsx #: apps/remix/app/components/dialogs/token-delete-dialog.tsx +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx #: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx @@ -1466,6 +1520,7 @@ msgstr "Może przygotować" #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx #: apps/remix/app/components/dialogs/passkey-create-dialog.tsx #: apps/remix/app/components/dialogs/document-resend-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx #: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx #: apps/remix/app/components/dialogs/document-delete-dialog.tsx @@ -1480,6 +1535,10 @@ msgstr "Anuluj" msgid "Cancelled by user" msgstr "Anulowano przez użytkownika" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Cannot remove document" +msgstr "" + #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Cannot remove signer" msgstr "Nie można usunąć sygnatariusza" @@ -1533,6 +1592,10 @@ msgstr "Wybierz odbiorcę bezpośredniego linku" msgid "Choose how the document will reach recipients" msgstr "Wybierz, jak dokument dotrze do odbiorców" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Choose how to distribute your document to recipients. Email will send notifications, None will generate signing links for manual distribution." +msgstr "" + #: apps/remix/app/components/forms/token.tsx msgid "Choose..." msgstr "Wybierz..." @@ -1602,6 +1665,10 @@ msgstr "Kliknij, aby wstawić pole" msgid "Close" msgstr "Zamknij" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Communication" +msgstr "" + #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/forms/signup.tsx @@ -1658,10 +1725,33 @@ msgstr "Dokumenty zakończone" msgid "Completed Documents" msgstr "Zakończone dokumenty" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "Completed on {formattedDate}" +msgstr "" + +#. placeholder {0}: parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[currentField.type]) +#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx +msgid "Configure {0} Field" +msgstr "" + +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Configure additional options and preferences" +msgstr "" + #: packages/lib/constants/template.ts msgid "Configure Direct Recipient" msgstr "Skonfiguruj bezpośredniego odbiorcę" +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Configure Document" +msgstr "" + +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +msgid "Configure Fields" +msgstr "" + #: apps/remix/app/components/general/document/document-edit-form.tsx msgid "Configure general settings for the document." msgstr "Skonfiguruj ogólne ustawienia dokumentu." @@ -1674,12 +1764,22 @@ msgstr "Skonfiguruj ogólne ustawienia szablonu." msgid "Configure template" msgstr "Skonfiguruj szablon" +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Configure Template" +msgstr "" + #. placeholder {0}: parseMessageDescriptor( _, FRIENDLY_FIELD_TYPE[currentField.type], ) +#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Configure the {0} field" msgstr "Skonfiguruj pole {0}" +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +msgid "Configure the fields you want to place on the document." +msgstr "" + #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx msgid "Confirm" @@ -1695,6 +1795,8 @@ msgstr "Potwierdź, wpisując <0>{deleteMessage}" #: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx #: apps/remix/app/components/dialogs/token-delete-dialog.tsx +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx msgid "Confirm by typing: <0>{deleteMessage}" msgstr "Potwierdź, wpisując: <0>{deleteMessage}" @@ -1724,12 +1826,13 @@ msgid "Content" msgstr "Treść" #: apps/remix/app/routes/_unauthenticated+/verify-email.$token.tsx -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx #: apps/remix/app/components/general/document-signing/document-signing-form.tsx +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx #: apps/remix/app/components/dialogs/passkey-create-dialog.tsx #: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx @@ -1940,6 +2043,7 @@ msgstr "Utworzono" msgid "Created At" msgstr "Utworzono w" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Created by" msgstr "Utworzono przez" @@ -1989,10 +2093,12 @@ msgstr "Tryb ciemny" msgid "Date" msgstr "Data" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Date created" msgstr "Data utworzenia" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Date Format" @@ -2042,8 +2148,11 @@ msgstr "Usuń" #. placeholder {0}: webhook.webhookUrl #. placeholder {0}: token.name +#. placeholder {0}: folder?.name ?? 'folder' #: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx #: apps/remix/app/components/dialogs/token-delete-dialog.tsx +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx msgid "delete {0}" msgstr "usuń {0}" @@ -2217,6 +2326,10 @@ msgstr "Wyświetl swoją nazwę i adres e-mail w dokumentach" msgid "Distribute Document" msgstr "Rozprowadź dokument" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx +msgid "Distribution Method" +msgstr "" + #: apps/remix/app/components/dialogs/template-delete-dialog.tsx msgid "Do you want to delete this template?" msgstr "Czy chcesz usunąć ten szablon?" @@ -2229,6 +2342,7 @@ msgstr "Czy chcesz zduplikować ten szablon?" msgid "Documenso will delete <0>all of your documents, along with all of your completed documents, signatures, and all other resources belonging to your Account." msgstr "Documenso usunie <0>wszystkie twoje dokumenty, wraz ze wszystkimi zakończonymi dokumentami, podpisami i wszystkimi innymi zasobami należącymi do twojego konta." +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx msgid "Document" @@ -2294,6 +2408,10 @@ msgstr "Dokument Zakończony!" msgid "Document created" msgstr "Dokument utworzony" +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Document Created" +msgstr "" + #. placeholder {0}: document.user.name #: apps/remix/app/components/general/template/template-page-view-recent-activity.tsx msgid "Document created by <0>{0}" @@ -2312,6 +2430,7 @@ msgstr "Dokument utworzony za pomocą <0>bezpośredniego linku" msgid "Document Creation" msgstr "Tworzenie dokumentu" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx #: apps/remix/app/components/dialogs/document-delete-dialog.tsx #: apps/remix/app/components/dialogs/admin-document-delete-dialog.tsx @@ -2344,12 +2463,18 @@ msgstr "Dokument zduplikowany" msgid "Document external ID updated" msgstr "Zaktualizowane ID zewnętrzne dokumentu" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "Document found in your account" +msgstr "" + +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx #: apps/remix/app/components/general/document/document-history-sheet.tsx msgid "Document history" msgstr "Historia dokumentu" #: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Document ID" msgstr "Identyfikator dokumentu" @@ -2358,6 +2483,14 @@ msgstr "Identyfikator dokumentu" msgid "Document inbox" msgstr "Skrzynka odbiorcza dokumentu" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Document is already uploaded" +msgstr "" + +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Document is using legacy field insertion" +msgstr "" + #: apps/remix/app/components/tables/templates-table.tsx msgid "Document Limit Exceeded!" msgstr "Przekroczono limit dokumentów!" @@ -2366,6 +2499,7 @@ msgstr "Przekroczono limit dokumentów!" msgid "Document metrics" msgstr "Metryki dokumentów" +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Document moved" msgstr "Dokument przeniesiony" @@ -2429,10 +2563,12 @@ msgstr "Zaktualizowano autoryzację podpisu dokumentu" msgid "Document signing process will be cancelled" msgstr "Proces podpisywania dokumentu zostanie anulowany" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Document status" msgstr "Status dokumentu" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Document title" msgstr "Tytuł dokumentu" @@ -2445,11 +2581,16 @@ msgstr "Zaktualizowano tytuł dokumentu" msgid "Document updated" msgstr "Zaktualizowano dokument" +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +msgid "Document updated successfully" +msgstr "" + #: apps/remix/app/components/general/document/document-upload.tsx msgid "Document upload disabled due to unpaid invoices" msgstr "Przesyłanie dokumentu wyłączone z powodu nieopłaconych faktur" #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "Document uploaded" msgstr "Przesłano dokument" @@ -2466,6 +2607,9 @@ msgid "Document will be permanently deleted" msgstr "Dokument zostanie trwale usunięty" #: apps/remix/app/routes/_profile+/p.$url.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId._index.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.edit.tsx +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.edit.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx @@ -2480,6 +2624,7 @@ msgstr "Dokument zostanie trwale usunięty" msgid "Documents" msgstr "Dokumenty" +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Documents created from template" msgstr "Dokumenty utworzone z szablonu" @@ -2500,6 +2645,7 @@ msgstr "Nie masz konta? <0>Zarejestruj się" #: apps/remix/app/components/tables/team-settings-billing-invoices-table.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx +#: apps/remix/app/components/general/share-document-download-button.tsx #: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx #: apps/remix/app/components/forms/2fa/view-recovery-codes-dialog.tsx @@ -2517,6 +2663,11 @@ msgstr "Pobierz dziennik logów" msgid "Download Certificate" msgstr "Pobierz certyfikat" +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx +#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx +msgid "Download Original" +msgstr "" + #: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx msgid "Download Template CSV" msgstr "Pobierz szablon CSV" @@ -2539,10 +2690,22 @@ msgstr "Szkice dokumentów" msgid "Drag & drop your PDF here." msgstr "Przeciągnij i upuść swój PDF tutaj." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Drag and drop or click to upload" +msgstr "" + +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +msgid "Drag and drop your PDF file here" +msgstr "" + #: packages/lib/constants/document.ts msgid "Draw" msgstr "Rysować" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Drop your document here" +msgstr "" + #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Dropdown" @@ -2577,6 +2740,7 @@ msgstr "Zduplikuj" msgid "Edit" msgstr "Edytuj" +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Edit Template" msgstr "Edytuj szablon" @@ -2607,6 +2771,9 @@ msgstr "Ujawnienie podpisu elektronicznego" #: apps/remix/app/components/forms/forgot-password.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx @@ -2700,6 +2867,7 @@ msgstr "Włącz niestandardowe brandowanie dla wszystkich dokumentów w tym zesp msgid "Enable Direct Link Signing" msgstr "Włącz podpisywanie linku bezpośredniego" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "Enable signing order" @@ -2746,6 +2914,10 @@ msgstr "Wprowadź swoje imię" msgid "Enter your text here" msgstr "Wprowadź swój tekst tutaj" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx #: apps/remix/app/components/general/verify-email-banner.tsx @@ -2780,10 +2952,15 @@ msgstr "Wprowadź swój tekst tutaj" #: apps/remix/app/components/general/document/document-edit-form.tsx #: apps/remix/app/components/general/document/document-edit-form.tsx #: apps/remix/app/components/general/document/document-edit-form.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx #: apps/remix/app/components/dialogs/webhook-create-dialog.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx #: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx #: apps/remix/app/components/dialogs/admin-user-enable-dialog.tsx #: apps/remix/app/components/dialogs/admin-user-disable-dialog.tsx @@ -2793,6 +2970,10 @@ msgstr "Wprowadź swój tekst tutaj" msgid "Error" msgstr "Błąd" +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "Error uploading file" +msgstr "" + #: apps/remix/app/components/forms/team-document-preferences-form.tsx msgid "Everyone can access and view the document" msgstr "Każdy może uzyskać dostęp do dokumentu i go wyświetlić" @@ -2823,6 +3004,11 @@ msgstr "Wygasa {0}" msgid "External ID" msgstr "Zewnętrzny ID" +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +msgid "Failed to create folder" +msgstr "" + #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx msgid "Failed to reseal document" msgstr "Nie udało się ponownie zaplombować dokumentu" @@ -2831,10 +3017,18 @@ msgstr "Nie udało się ponownie zaplombować dokumentu" msgid "Failed to save settings." msgstr "Nie udało się zapisać ustawień." +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +msgid "Failed to update document" +msgstr "" + #: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx msgid "Failed to update recipient" msgstr "Nie udało się zaktualizować odbiorcy" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +msgid "Failed to update template" +msgstr "" + #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id.tsx #: apps/remix/app/routes/_authenticated+/settings+/webhooks.$id.tsx msgid "Failed to update webhook" @@ -2887,7 +3081,13 @@ msgstr "Pole niepodpisane" msgid "Fields" msgstr "Pola" +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Fields updated" +msgstr "" + #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx msgid "File cannot be larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB" msgstr "Plik nie może mieć większej wielkości niż {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB" @@ -2895,6 +3095,25 @@ msgstr "Plik nie może mieć większej wielkości niż {APP_DOCUMENT_UPLOAD_SIZE msgid "File size exceeds the limit of {APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB" msgstr "Rozmiar pliku przekracza limit {APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Folder" +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +msgid "Folder created successfully" +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx +#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx +msgid "Folder not found" +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx +#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx +msgid "Folder updated successfully" +msgstr "" + #: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx #: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx #: packages/ui/primitives/document-flow/field-items-advanced-settings/name-field.tsx @@ -2941,6 +3160,7 @@ msgstr "Imię i nazwisko" #: apps/remix/app/components/general/teams/team-settings-nav-desktop.tsx #: apps/remix/app/components/general/document/document-edit-form.tsx #: apps/remix/app/components/general/direct-template/direct-template-page.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx msgid "General" msgstr "Ogólne" @@ -2971,6 +3191,10 @@ msgstr "Wróć do domu" msgid "Go Back Home" msgstr "Wróć do domu" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "Go to document" +msgstr "" + #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx msgid "Go to owner" msgstr "Przejdź do właściciela" @@ -3142,7 +3366,7 @@ msgstr "Nieprawidłowy kod. Proszę spróbuj ponownie." msgid "Invalid email" msgstr "Nieprawidłowy email" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx msgid "Invalid link" msgstr "Nieprawidłowy link" @@ -3239,6 +3463,7 @@ msgid "Label" msgstr "Etykieta" #: apps/remix/app/components/general/menu-switcher.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Language" @@ -3261,6 +3486,7 @@ msgstr "Ostatnie 7 dni" msgid "Last modified" msgstr "Ostatnia modyfikacja" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Last updated" msgstr "Zaktualizowano" @@ -3363,6 +3589,7 @@ msgstr "Zarządzaj profilem {0}" msgid "Manage all teams you are currently associated with." msgstr "Zarządzaj wszystkimi zespołami, z którymi jesteś obecnie związany." +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Manage and view template" msgstr "Zarządzaj i przeglądaj szablon" @@ -3471,6 +3698,7 @@ msgstr "Data dołączenia" msgid "Members" msgstr "Członkowie" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-subject.tsx msgid "Message <0>(Optional)" @@ -3498,19 +3726,38 @@ msgstr "Miesięczni aktywni użytkownicy: Użytkownicy, którzy utworzyli przyna msgid "Monthly Active Users: Users that had at least one of their documents completed" msgstr "Miesięczni aktywni użytkownicy: Użytkownicy, którzy mieli przynajmniej jeden z ukończonych dokumentów" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Move" msgstr "Przenieś" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "Move \"{templateTitle}\" to a folder" +msgstr "" + +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Move Document to Folder" +msgstr "" + #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Move Document to Team" msgstr "Przenieś dokument do zespołu" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "Move Template to Folder" +msgstr "" + #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "Move Template to Team" msgstr "Przenieś szablon do zespołu" +#: apps/remix/app/components/tables/templates-table-action-dropdown.tsx +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx +msgid "Move to Folder" +msgstr "" + #: apps/remix/app/components/tables/templates-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx msgid "Move to Team" @@ -3534,6 +3781,8 @@ msgstr "Moje szablony" #: apps/remix/app/components/general/claim-account.tsx #: apps/remix/app/components/general/document-signing/document-signing-name-field.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/template-use-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx @@ -3622,8 +3871,8 @@ msgstr "Brak ostatnich aktywności" msgid "No recent documents" msgstr "Brak ostatnich dokumentów" +#: packages/ui/primitives/recipient-selector.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx -#: packages/ui/primitives/document-flow/add-fields.tsx msgid "No recipient matching this description was found." msgstr "Nie znaleziono odbiorcy pasującego do tego opisu." @@ -3634,8 +3883,8 @@ msgstr "Nie znaleziono odbiorcy pasującego do tego opisu." msgid "No recipients" msgstr "Brak odbiorców" +#: packages/ui/primitives/recipient-selector.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx -#: packages/ui/primitives/document-flow/add-fields.tsx msgid "No recipients with this role" msgstr "Brak odbiorców z tą rolą" @@ -3677,6 +3926,7 @@ msgstr "Nie znaleziono wartości." msgid "No worries, it happens! Enter your email and we'll email you a special link to reset your password." msgstr "Nie martw się, to się zdarza! Wprowadź swój e-mail, a my wyślemy Ci specjalny link do zresetowania hasła." +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/lib/constants/document.ts msgid "None" msgstr "Brak" @@ -3900,8 +4150,8 @@ msgstr "Płatność zaległa" #: apps/remix/app/components/tables/user-settings-teams-page-table.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx #: apps/remix/app/components/general/document/document-status.tsx -#: apps/remix/app/components/general/document/document-read-only-fields.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx +#: packages/ui/components/document/document-read-only-fields.tsx #: packages/lib/constants/document.ts msgid "Pending" msgstr "Oczekujące" @@ -3989,6 +4239,11 @@ msgstr "Proszę sprawdzić swój email w celu aktualizacji." msgid "Please choose your new password" msgstr "Proszę wybrać nowe hasło" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +msgid "Please configure the document first" +msgstr "" + #: packages/lib/server-only/auth/send-confirmation-email.ts msgid "Please confirm your email" msgstr "Proszę potwierdzić swój email" @@ -4261,6 +4516,7 @@ msgstr "Odbiorca zaktualizowany" #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx #: apps/remix/app/components/general/template/template-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx msgid "Recipients" msgstr "Odbiorcy" @@ -4284,6 +4540,7 @@ msgstr "Kody odzyskiwania" msgid "Red" msgstr "Czerwony" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Redirect URL" @@ -4345,7 +4602,6 @@ msgstr "Przypomnienie: Proszę {recipientActionVerb} Twój dokument" #: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx #: apps/remix/app/components/general/teams/team-email-dropdown.tsx #: apps/remix/app/components/general/document-signing/document-signing-field-container.tsx -#: apps/remix/app/components/general/document-signing/document-signing-field-container.tsx #: apps/remix/app/components/forms/team-branding-preferences-form.tsx #: apps/remix/app/components/forms/avatar-image.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx @@ -4442,7 +4698,7 @@ msgstr "Przechowywanie dokumentów" msgid "Retry" msgstr "Spróbuj ponownie" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx #: apps/remix/app/routes/_unauthenticated+/team.decline.$token.tsx @@ -4473,6 +4729,7 @@ msgstr "Cofnij dostęp" #: apps/remix/app/components/tables/user-settings-current-teams-table.tsx #: apps/remix/app/components/tables/team-settings-members-table.tsx #: apps/remix/app/components/tables/team-settings-member-invites-table.tsx +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx #: apps/remix/app/components/dialogs/team-member-invite-dialog.tsx @@ -4484,12 +4741,19 @@ msgstr "Rola" msgid "Roles" msgstr "Role" +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Root (No Folder)" +msgstr "" + #: packages/ui/primitives/data-table-pagination.tsx msgid "Rows per page" msgstr "Wiersze na stronę" #: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx #: apps/remix/app/components/general/document-signing/document-signing-number-field.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx +#: apps/remix/app/components/embed/authoring/configure-fields-view.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx msgid "Save" @@ -4544,6 +4808,10 @@ msgstr "Aktywność bezpieczeństwa" msgid "Select" msgstr "Wybierz" +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "Select a folder to move this document to." +msgstr "" + #: apps/remix/app/components/dialogs/template-move-dialog.tsx #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "Select a team" @@ -4665,6 +4933,14 @@ msgstr "Wysłano" msgid "Set a password" msgstr "Ustaw hasło" +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Set up your document properties and recipient information" +msgstr "" + +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx +msgid "Set up your template properties and recipient information" +msgstr "" + #: apps/remix/app/routes/_authenticated+/settings+/_layout.tsx #: apps/remix/app/components/general/app-nav-mobile.tsx #: apps/remix/app/components/general/app-command-menu.tsx @@ -4806,7 +5082,6 @@ msgstr "Zarejestruj się za pomocą OIDC" #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/document-flow/types.ts -#: packages/ui/primitives/document-flow/field-icon.tsx #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Signature" msgstr "Podpis" @@ -4836,8 +5111,8 @@ msgid "Signatures will appear once the document has been completed" msgstr "Podpisy pojawią się po ukończeniu dokumentu" #: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx -#: apps/remix/app/components/general/document/document-read-only-fields.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx +#: packages/ui/components/document/document-read-only-fields.tsx #: packages/lib/constants/recipient-roles.ts msgid "Signed" msgstr "Podpisano" @@ -4934,7 +5209,9 @@ msgstr "Niektórzy sygnatariusze nie zostali przypisani do pola podpisu. Przypis #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx +#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx +#: apps/remix/app/components/general/share-document-download-button.tsx #: apps/remix/app/components/general/billing-portal-button.tsx #: apps/remix/app/components/general/billing-plans.tsx #: apps/remix/app/components/general/teams/team-layout-billing-banner.tsx @@ -4946,6 +5223,7 @@ msgstr "Niektórzy sygnatariusze nie zostali przypisani do pola podpisu. Przypis #: apps/remix/app/components/general/document-signing/document-signing-auth-page.tsx #: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx #: apps/remix/app/components/general/document/document-page-view-dropdown.tsx +#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx #: apps/remix/app/components/general/document/document-certificate-download-button.tsx #: apps/remix/app/components/general/document/document-audit-log-download-button.tsx @@ -4972,7 +5250,7 @@ msgid "Something went wrong" msgstr "Coś poszło nie tak" #. placeholder {0}: data.teamName -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Something went wrong while attempting to transfer the ownership of team <0>{0} to your. Please try again later or contact support." msgstr "Coś poszło nie tak podczas próby przeniesienia własności zespołu <0>{0} do Ciebie. Proszę spróbować ponownie później lub skontaktować się z pomocą techniczną." @@ -5044,6 +5322,7 @@ msgstr "Stan" msgid "Step <0>{step} of {maxStep}" msgstr "Krok <0>{step} z {maxStep}" +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-subject.tsx msgid "Subject <0>(Optional)" @@ -5061,6 +5340,8 @@ msgstr "Subskrypcja" msgid "Subscriptions" msgstr "Subskrypcje" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx #: apps/remix/app/components/tables/user-settings-pending-teams-table-actions.tsx #: apps/remix/app/components/tables/team-settings-member-invites-table.tsx @@ -5201,15 +5482,15 @@ msgstr "Tylko dla zespołu" msgid "Team only templates are not linked anywhere and are visible only to your team." msgstr "Szablony tylko dla zespołu nie są nigdzie linkowane i są widoczne tylko dla Twojego zespołu." -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Team ownership transfer" msgstr "Przeniesienie własności zespołu" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Team ownership transfer already completed!" msgstr "Transfer własności zespołu został już zakończony!" -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "Team ownership transferred!" msgstr "Własność zespołu przeniesiona!" @@ -5258,6 +5539,8 @@ msgstr "Zespoły" msgid "Teams restricted" msgstr "Zespoły ograniczone" +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id.edit.tsx +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id.edit.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx @@ -5268,6 +5551,10 @@ msgstr "Zespoły ograniczone" msgid "Template" msgstr "Szablon" +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Template Created" +msgstr "" + #: apps/remix/app/components/dialogs/template-delete-dialog.tsx msgid "Template deleted" msgstr "Szablon usunięty" @@ -5288,6 +5575,11 @@ msgstr "Szablon został usunięty z profilu publicznego." msgid "Template has been updated." msgstr "Szablon został zaktualizowany." +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Template is using legacy field insertion" +msgstr "" + +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "Template moved" msgstr "Szablon przeniesiony" @@ -5304,6 +5596,12 @@ msgstr "Szablon zapisany" msgid "Template title" msgstr "Tytuł szablonu" +#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx +msgid "Template updated successfully" +msgstr "" + +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx #: apps/remix/app/components/general/app-nav-mobile.tsx @@ -5379,10 +5677,18 @@ msgstr "Treść do wyświetlenia w banerze, dozwolone HTML" msgid "The direct link has been copied to your clipboard" msgstr "Bezpośredni link został skopiowany do schowka" +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "The document has been moved successfully." +msgstr "" + #: apps/remix/app/components/dialogs/document-move-dialog.tsx msgid "The document has been successfully moved to the selected team." msgstr "Dokument został pomyślnie przeniesiony do wybranego zespołu." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "The document is already saved and cannot be changed." +msgstr "" + #: apps/remix/app/components/embed/embed-document-completed.tsx msgid "The document is now completed, please follow any instructions provided within the parent application." msgstr "Dokument jest teraz zakończony, proszę postępować zgodnie z wszelkimi instrukcjami podanymi w aplikacji nadrzędnej." @@ -5421,6 +5727,28 @@ msgstr "Podany e-mail lub hasło jest nieprawidłowe" msgid "The events that will trigger a webhook to be sent to your URL." msgstr "Wydarzenia, które wyzwolą webhook do wysłania do Twojego URL." +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "The fields have been updated to the new field insertion method successfully" +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx +msgid "The folder you are trying to delete does not exist." +msgstr "" + +#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx +#: apps/remix/app/components/dialogs/folder-move-dialog.tsx +msgid "The folder you are trying to move does not exist." +msgstr "" + +#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +msgid "The folder you are trying to move the document to does not exist." +msgstr "" + +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "The folder you are trying to move the template to does not exist." +msgstr "" + #: packages/email/templates/bulk-send-complete.tsx msgid "The following errors occurred:" msgstr "Wystąpiły następujące błędy:" @@ -5434,7 +5762,7 @@ msgid "The following team has been deleted by you" msgstr "Następujący zespół został usunięty przez Ciebie" #. placeholder {0}: data.teamName -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "The ownership of team <0>{0} has been successfully transferred to you." msgstr "Własność zespołu <0>{0} została pomyślnie przeniesiona na Ciebie." @@ -5531,10 +5859,15 @@ msgid "The team transfer request to <0>{0} has expired." msgstr "Prośba o przeniesienie zespołu do <0>{0} wygasła." #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/_layout.tsx -msgid "The team you are looking for may have been removed, renamed or may have never\n" +msgid "" +"The team you are looking for may have been removed, renamed or may have never\n" " existed." msgstr "Zespół, którego szukasz, mógł zostać usunięty, zmieniony na, albo nigdy nie istniał." +#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +msgid "The template has been moved successfully." +msgstr "" + #: apps/remix/app/components/dialogs/template-move-dialog.tsx msgid "The template has been successfully moved to the selected team." msgstr "Szablon został pomyślnie przeniesiony do wybranego zespołu." @@ -5590,6 +5923,10 @@ msgstr "Brak aktywnych szkiców. Prześlij, aby utworzyć." msgid "There are no completed documents yet. Documents that you have created or received will appear here once completed." msgstr "Brak zakończonych dokumentów. Utworzone lub odebrane dokumentu pojawią się tutaj, gdy zostaną zakończone." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "There was an error uploading your file. Please try again." +msgstr "" + #: apps/remix/app/components/general/teams/team-email-usage.tsx msgid "They have permission on your behalf to:" msgstr "Mają pozwolenie w Twoim imieniu na:" @@ -5620,6 +5957,10 @@ msgstr "To można nadpisać, ustawiając wymagania dotyczące uwierzytelniania b msgid "This document can not be recovered, if you would like to dispute the reason for future documents please contact support." msgstr "Dokument ten nie może być odzyskany. Jeśli chcesz zakwestionować przyczynę przyszłych dokumentów, skontaktuj się z administracją." +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +msgid "This document cannot be changed" +msgstr "" + #: apps/remix/app/components/dialogs/document-delete-dialog.tsx msgid "This document could not be deleted at this time. Please try again." msgstr "Nie można usunąć tego dokumentu w tej chwili. Proszę spróbować ponownie." @@ -5632,7 +5973,7 @@ msgstr "Nie można skopiować tego dokumentu w tej chwili. Proszę spróbować p msgid "This document could not be re-sent at this time. Please try again." msgstr "Nie można ponownie wysłać tego dokumentu w tej chwili. Proszę spróbować ponownie." -#: packages/ui/primitives/document-flow/add-fields.tsx +#: packages/ui/primitives/recipient-selector.tsx msgid "This document has already been sent to this recipient. You can no longer edit this recipient." msgstr "Ten dokument został już wysłany do tego odbiorcy. Nie można już edytować tego odbiorcy." @@ -5644,18 +5985,29 @@ msgstr "Ten dokument został anulowany przez właściciela i nie jest już dost msgid "This document has been cancelled by the owner." msgstr "Ten dokument został anulowany przez właściciela." +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "This document has been rejected by a recipient" msgstr "Dokument został odrzucony przez odbiorcę" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "This document has been signed by all recipients" msgstr "Ten dokument został podpisany przez wszystkich odbiorców" +#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx +msgid "This document is available in your Documenso account. You can view more details, recipients, and audit logs there." +msgstr "" + +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx msgid "This document is currently a draft and has not been sent" msgstr "Ten dokument jest obecnie szkicowany i nie został wysłany" +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "This document is using legacy field insertion, we recommend using the new field insertion method for more accurate results." +msgstr "" + #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx msgid "This document was created by you or a team member using the template above." msgstr "Ten dokument został stworzony przez Ciebie lub członka zespołu przy użyciu powyższego szablonu." @@ -5696,11 +6048,16 @@ msgstr "Ten e-mail zostanie wysłany do odbiorcy, który właśnie podpisał dok msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them." msgstr "To pole nie może być modyfikowane ani usuwane. Po udostępnieniu bezpośredniego linku do tego szablonu lub dodaniu go do swojego publicznego profilu, każdy, kto się w nim dostanie, może wpisać swoje imię i email oraz wypełnić przypisane mu pola." +#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx +#: apps/remix/app/components/dialogs/folder-create-dialog.tsx +msgid "This folder name is already taken." +msgstr "" + #: packages/ui/primitives/template-flow/add-template-settings.tsx msgid "This is how the document will reach the recipients once the document is ready for signing." msgstr "W ten sposób dokument dotrze do odbiorców, gdy tylko dokument będzie gotowy do podpisania." -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "This link is invalid or has expired. Please contact your team to resend a transfer request." msgstr "Ten link jest nieprawidłowy lub wygasł. Proszę skontaktować się ze swoim zespołem, aby ponownie wysłać prośbę o transfer." @@ -5740,6 +6097,10 @@ msgstr "Ten zespół oraz wszelkie powiązane dane, z wyjątkiem faktur, zostan msgid "This template could not be deleted at this time. Please try again." msgstr "Ten szablon nie mógł zostać usunięty w tej chwili. Proszę spróbować ponownie." +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "This template is using legacy field insertion, we recommend using the new field insertion method for more accurate results." +msgstr "" + #: apps/remix/app/routes/_unauthenticated+/team.decline.$token.tsx msgid "This token is invalid or has expired. No action is needed." msgstr "Ten token jest nieprawidłowy lub wygasł. Nie wymaga żadnej akcji." @@ -5778,11 +6139,13 @@ msgstr "To zastąpi wszystkie globalne ustawienia." msgid "Time" msgstr "Czas" +#: apps/remix/app/routes/_authenticated+/documents.f.$folderId.$id.logs.tsx #: apps/remix/app/routes/_authenticated+/documents.$id.logs.tsx msgid "Time zone" msgstr "Strefa czasowa" #: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx +#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Time Zone" @@ -5792,6 +6155,7 @@ msgstr "Strefa czasowa" #: apps/remix/app/components/tables/templates-table.tsx #: apps/remix/app/components/tables/documents-table.tsx #: apps/remix/app/components/general/template/template-page-view-documents-table.tsx +#: apps/remix/app/components/embed/authoring/configure-document-view.tsx #: packages/ui/primitives/document-flow/add-settings.tsx msgid "Title" msgstr "Tytuł" @@ -6101,6 +6465,10 @@ msgstr "Zaktualizuj" msgid "Update Banner" msgstr "Zaktualizuj baner" +#: apps/remix/app/components/general/legacy-field-warning-popover.tsx +msgid "Update Fields" +msgstr "" + #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx msgid "Update passkey" msgstr "Zaktualizuj klucz dostępu" @@ -6115,6 +6483,7 @@ msgstr "Zaktualizuj profil" #: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx +#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx msgid "Update Recipient" msgstr "Zaktualizuj odbiorcę" @@ -6157,10 +6526,15 @@ msgstr "Aktualizowanie hasła..." msgid "Updating Your Information" msgstr "Aktualizacja Twoich informacji" +#: packages/ui/primitives/document-upload.tsx #: packages/ui/primitives/document-dropzone.tsx msgid "Upgrade" msgstr "Ulepsz" +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +msgid "Upgrade your plan to upload more documents" +msgstr "" + #: packages/lib/constants/document.ts msgid "Upload" msgstr "Prześlij" @@ -6189,10 +6563,17 @@ msgstr "Prześlij CSV" msgid "Upload custom document" msgstr "Prześlij niestandardowy dokument" +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx +#: packages/ui/primitives/document-upload.tsx +msgid "Upload Document" +msgstr "" + #: packages/ui/primitives/signature-pad/signature-pad-upload.tsx msgid "Upload Signature" msgstr "Prześlij podpis" +#: packages/ui/primitives/document-upload.tsx #: packages/ui/primitives/document-dropzone.tsx msgid "Upload Template Document" msgstr "Prześlij dokument szablonu" @@ -6218,6 +6599,11 @@ msgstr "Przesłany plik jest zbyt mały" msgid "Uploaded file not an allowed file type" msgstr "Przesłany plik nie jest dozwolonym typem pliku" +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +msgid "Uploading document..." +msgstr "" + +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId.$id._index.tsx #: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx msgid "Use" msgstr "Użyj" @@ -6666,6 +7052,7 @@ msgstr "Wygenerujemy dla Ciebie linki do podpisania, które możesz wysłać do msgid "We won't send anything to notify recipients." msgstr "Nie wyślemy nic, aby powiadomić odbiorców." +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx #: apps/remix/app/routes/_authenticated+/templates._index.tsx #: apps/remix/app/components/tables/documents-table-empty-state.tsx msgid "We're all empty" @@ -6730,6 +7117,7 @@ msgstr "Witamy w Documenso!" msgid "Were you trying to edit this document instead?" msgstr "Czy próbowałeś raczej edytować ten dokument?" +#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "When enabled, signers can choose who should sign next in the sequence instead of following the predefined order." @@ -6889,11 +7277,13 @@ msgstr "Nie możesz mieć więcej niż {MAXIMUM_PASSKEYS} kluczy zabezpieczeń." msgid "You cannot modify a team member who has a higher role than you." msgstr "Nie możesz modyfikować członka zespołu, który ma wyższą rolę niż ty." +#: packages/ui/primitives/document-upload.tsx #: packages/ui/primitives/document-dropzone.tsx msgid "You cannot upload documents at this time." msgstr "Nie możesz przesyłać dokumentów w tej chwili." #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "You cannot upload encrypted PDFs" msgstr "Nie możesz przesyłać zaszyfrowanych plików PDF" @@ -6915,7 +7305,7 @@ msgid "You have accepted an invitation from <0>{0} to join their team." msgstr "Zaakceptowałeś zaproszenie od <0>{0}, aby dołączyć do ich zespołu." #. placeholder {0}: data.teamName -#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx +#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx msgid "You have already completed the ownership transfer for <0>{0}." msgstr "Już zakończyłeś transfer własności dla <0>{0}." @@ -6965,6 +7355,7 @@ msgstr "Rozpocząłeś dokument {0}, który wymaga, abyś go {recipientActionVer msgid "You have no webhooks yet. Your webhooks will be shown here once you create them." msgstr "Nie masz jeszcze żadnych webhooków. Twoje webhooki będą tutaj widoczne, gdy je utworzysz." +#: apps/remix/app/routes/_authenticated+/templates.f.$folderId._index.tsx #: apps/remix/app/routes/_authenticated+/templates._index.tsx msgid "You have not yet created any templates. To create a template please upload one." msgstr "Brak utworzonych szablonów. Prześlij, aby utworzyć." @@ -6979,6 +7370,7 @@ msgid "You have reached the maximum limit of {0} direct templates. <0>Upgrade yo msgstr "Osiągnąłeś maksymalny limit {0} bezpośrednich szablonów. <0>Ulepsz swoje konto, aby kontynuować!" #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "You have reached your document limit for this month. Please upgrade your plan." msgstr "Osiągnąłeś limit dokumentów na ten miesiąc. Proszę zaktualizować swój plan." @@ -7052,6 +7444,11 @@ msgstr "Musisz mieć przynajmniej jednego innego członka zespołu, aby przenie msgid "You must set a profile URL before enabling your public profile." msgstr "Musisz ustawić URL profilu przed włączeniem swojego publicznego profilu." +#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx +#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx +msgid "You must type '{deleteMessage}' to confirm" +msgstr "" + #: apps/remix/app/routes/_authenticated+/settings+/tokens.tsx msgid "You need to be an admin to manage API tokens." msgstr "Musisz być administratorem, aby zarządzać tokenami API." @@ -7117,6 +7514,8 @@ msgid "Your direct signing templates" msgstr "Twoje bezpośrednie szablony podpisu" #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx msgid "Your document failed to upload." msgstr "Twój dokument nie udało się załadować." @@ -7124,6 +7523,10 @@ msgstr "Twój dokument nie udało się załadować." msgid "Your document has been created from the template successfully." msgstr "Twój dokument został pomyślnie utworzony na podstawie szablonu." +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Your document has been created successfully" +msgstr "" + #: packages/email/template-components/template-document-super-delete.tsx msgid "Your document has been deleted by an admin!" msgstr "Dokument został usunięty przez administratora!" @@ -7141,6 +7544,7 @@ msgid "Your document has been successfully duplicated." msgstr "Twój dokument został pomyślnie zduplikowany." #: apps/remix/app/components/general/document/document-upload.tsx +#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx msgid "Your document has been uploaded successfully." msgstr "Twój dokument został pomyślnie załadowany." @@ -7234,6 +7638,10 @@ msgstr "Twój zespół został pomyślnie usunięty." msgid "Your team has been successfully updated." msgstr "Twój zespół został pomyślnie zaktualizowany." +#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx +msgid "Your template has been created successfully" +msgstr "" + #: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx msgid "Your template has been duplicated successfully." msgstr "Twój szablon został pomyślnie zduplikowany." @@ -7261,4 +7669,3 @@ msgstr "Twój token został pomyślnie utworzony! Upewnij się, że go skopiujes #: apps/remix/app/routes/_authenticated+/settings+/tokens.tsx msgid "Your tokens will be shown here once you create them." msgstr "Twoje tokeny będą tutaj wyświetlane po ich utworzeniu." - From ab323f149f125f9e525c1f65846c23f43284bc40 Mon Sep 17 00:00:00 2001 From: Mythie Date: Sat, 3 May 2025 09:23:17 +1000 Subject: [PATCH 11/12] fix: resolve issue with uploading templates --- packages/lib/server-only/template/create-template.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/server-only/template/create-template.ts b/packages/lib/server-only/template/create-template.ts index 6d4681724..254146b6c 100644 --- a/packages/lib/server-only/template/create-template.ts +++ b/packages/lib/server-only/template/create-template.ts @@ -73,7 +73,7 @@ export const createTemplate = async ({ } } - if (!team) { + if (teamId && !team) { throw new AppError(AppErrorCode.NOT_FOUND); } From e40c5d9d248750ed1c17eeb011ce44ebf04c2aba Mon Sep 17 00:00:00 2001 From: Mythie Date: Sat, 3 May 2025 09:23:25 +1000 Subject: [PATCH 12/12] v1.10.3 --- apps/remix/package.json | 2 +- package-lock.json | 6 +++--- package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/remix/package.json b/apps/remix/package.json index 628fd35fc..c2b4f3899 100644 --- a/apps/remix/package.json +++ b/apps/remix/package.json @@ -100,5 +100,5 @@ "vite-plugin-babel-macros": "^1.0.6", "vite-tsconfig-paths": "^5.1.4" }, - "version": "1.10.2" + "version": "1.10.3" } diff --git a/package-lock.json b/package-lock.json index b53fda783..2e15eeb19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@documenso/root", - "version": "1.10.2", + "version": "1.10.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@documenso/root", - "version": "1.10.2", + "version": "1.10.3", "workspaces": [ "apps/*", "packages/*" @@ -95,7 +95,7 @@ }, "apps/remix": { "name": "@documenso/remix", - "version": "1.10.2", + "version": "1.10.3", "dependencies": { "@documenso/api": "*", "@documenso/assets": "*", diff --git a/package.json b/package.json index 37f9193fe..99cad30dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "1.10.2", + "version": "1.10.3", "scripts": { "build": "turbo run build", "dev": "turbo run dev --filter=@documenso/remix",