Compare commits

..

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 46d712f4cc Initial plan 2026-02-12 04:07:09 +00:00
David Nguyen 62c609f105 fix: completed audit logs 2026-02-12 15:04:16 +11:00
Copilot 4babe9b192 fix: use rejection audit log for IP/Device fields on rejected certificates (#2479) 2026-02-12 13:01:55 +11:00
David Nguyen 7e422bc3fd fix: highlight rejected certificate text 2026-02-12 12:23:00 +11:00
85 changed files with 1911 additions and 5864 deletions
@@ -1,168 +0,0 @@
---
date: 2026-02-11
title: Cert Page Width Mismatch
---
## Problem
Certificate and audit log pages are generated with hardcoded A4 dimensions (`PDF_SIZE_A4_72PPI`: 595×842) regardless of the actual document page sizes. When the source document uses a different page size (e.g., Letter, Legal, or custom dimensions), the certificate/audit log pages end up with a different width than the document pages. This causes problems with courts that expect uniform page dimensions throughout a PDF.
**Both width and height must match** the last page of the document so the entire PDF prints uniformly.
**Root cause**: In `seal-document.handler.ts` (lines 186-187), the certificate payload always uses:
```ts
pageWidth: PDF_SIZE_A4_72PPI.width, // 595
pageHeight: PDF_SIZE_A4_72PPI.height, // 842
```
These hardcoded values flow into `generateCertificatePdf`, `generateAuditLogPdf`, `renderCertificate`, and `renderAuditLogs` — all of which use `pageWidth`/`pageHeight` to set Konva stage dimensions and layout content.
## Key Files
| File | Role |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `packages/lib/jobs/definitions/internal/seal-document.handler.ts` | Orchestrates sealing; passes page dimensions to cert/audit generators |
| `packages/lib/constants/pdf.ts` | Defines `PDF_SIZE_A4_72PPI` (595×842) |
| `packages/lib/server-only/pdf/generate-certificate-pdf.ts` | Generates certificate PDF; accepts `pageWidth`/`pageHeight` |
| `packages/lib/server-only/pdf/generate-audit-log-pdf.ts` | Generates audit log PDF; accepts `pageWidth`/`pageHeight` |
| `packages/lib/server-only/pdf/render-certificate.ts` | Renders certificate pages via Konva; uses `pageWidth`/`pageHeight` for stage + layout |
| `packages/lib/server-only/pdf/render-audit-logs.ts` | Renders audit log pages via Konva; uses `pageWidth`/`pageHeight` for stage + layout |
| `packages/lib/server-only/pdf/get-page-size.ts` | Existing utility — extend with `@libpdf/core` version |
| `packages/trpc/server/document-router/download-document-certificate.ts` | Standalone certificate download (also hardcodes A4) |
| `packages/trpc/server/document-router/download-document-audit-logs.ts` | Standalone audit log download (also hardcodes A4) |
## Architecture
### Current Flow
1. **One cert PDF + one audit log PDF** generated per envelope with hardcoded A4 dims
2. Both appended to **every** envelope item (document) via `decorateAndSignPdf``pdfDoc.copyPagesFrom()`
3. The audit log is envelope-level (all recipients, all events across all docs) — one per envelope, not per document
### Multi-Document Envelopes
- V1 envelopes: single document only
- V2 envelopes: support multiple documents (envelope items)
- Each envelope item gets both cert + audit log pages appended to it
- If documents have different page sizes → need size-matched cert/audit for each
### Reading Page Dimensions (`@libpdf/core` only)
Use `@libpdf/core`'s `PDF` class — NOT `@cantoo/pdf-lib`:
```ts
const pdfDoc = await PDF.load(pdfData);
const lastPage = pdfDoc.getPage(pdfDoc.getPageCount() - 1);
const { width, height } = lastPage; // e.g. 612, 792 for Letter
```
Already used this way in `seal-document.handler.ts` lines 403-410 for V2 field insertion.
"Last page" = last page of the original document, before cert/audit pages are appended.
### Content Layout Adaptation
Both renderers already handle variable dimensions gracefully:
- **Width**: `render-certificate.ts:713` / `render-audit-logs.ts:588``Math.min(pageWidth - minimumMargin * 2, contentMaxWidth)` with `contentMaxWidth = 768`. Wider pages get more margin, narrower pages tighter margins.
- **Height**: Both renderers paginate content into pages using `groupRowsIntoPages()` which respects `pageHeight` via `maxTableHeight = pageHeight - pageTopMargin - pageBottomMargin`. Shorter pages just mean more pages; taller pages fit more rows per page.
### Playwright PDF Path — Out of Scope
The `NEXT_PRIVATE_USE_PLAYWRIGHT_PDF` toggle enables a deprecated Playwright-based PDF generation path (`get-certificate-pdf.ts`, `get-audit-logs-pdf.ts`) that also hardcodes `format: 'A4'` in `page.pdf()`. This path is **not being updated** as part of this fix:
- Both files are marked `@deprecated`
- The Konva-based path is the default and recommended path
- The Playwright path is behind a feature flag and will be removed
No changes needed. Add a code comment noting the A4 limitation if the Playwright path is ever re-enabled.
## Plan
### 1. Extend `get-page-size.ts` with `@libpdf/core` utility
Add a `getLastPageDimensions` function to the existing `packages/lib/server-only/pdf/get-page-size.ts` file. This consolidates page-size logic in one place (the file already has the legacy `@cantoo/pdf-lib` version).
```ts
export const getLastPageDimensions = (pdfDoc: PDF): { width: number; height: number } => {
const lastPage = pdfDoc.getPage(pdfDoc.getPageCount() - 1);
const width = Math.round(lastPage.width);
const height = Math.round(lastPage.height);
if (width < MIN_CERT_PAGE_WIDTH || height < MIN_CERT_PAGE_HEIGHT) {
return { width: PDF_SIZE_A4_72PPI.width, height: PDF_SIZE_A4_72PPI.height };
}
return { width, height };
};
```
**Dimension rounding**: `Math.round()` both width and height. PDF points at 72ppi are typically whole numbers; rounding avoids spurious float-precision mismatches (e.g., 612.0 vs 612.00001) that would cause unnecessary duplicate cert/audit PDF generation.
**Minimum page dimensions**: Enforce a minimum threshold (e.g., 300pt for both width and height). If either dimension falls below the minimum, fall back to A4 (595×842). The certificate and audit log renderers have headers, table rows, margins, and QR codes that require a minimum viable area.
### 2. Read last page dimensions from each envelope item's PDF
In `seal-document.handler.ts`, before generating cert/audit PDFs:
- For each `envelopeItem`, load the PDF and read the **last page's width and height** using `getLastPageDimensions`
- Use `PDF.load()` then pass the loaded doc to the utility
**Resealing consideration**: When `isResealing` is true, envelope items are remapped to use `initialData` (lines 152-158) before this point. Page-size extraction must operate on the same data source that `decorateAndSignPdf` will use. Since the `envelopeItems` array is already remapped by the time we read dimensions, reading from `envelopeItem.documentData` will naturally give the correct (initial) data. No special handling needed beyond ensuring the dimension read happens **after** the resealing remap.
### 3. Generate cert/audit PDFs per unique page size
Current flow generates one cert + one audit log doc per envelope. Change to:
1. Collect `{ width, height }` of the last page for each envelope item
2. Deduplicate by `"${width}x${height}"` key (using the already-rounded integers)
3. For each unique size, generate cert PDF and audit log PDF with those dimensions
4. Store in a `Map<string, { certificateDoc, auditLogDoc }>` keyed by `"${width}x${height}"`
For the common single-document case, this is one generation — same perf as today.
### 4. Thread the correct docs into `decorateAndSignPdf`
In the envelope item loop, look up the item's last-page dimensions in the map and pass the matching cert/audit docs. Signature of `decorateAndSignPdf` doesn't change — it still receives a single `certificateDoc` and `auditLogDoc`, just the right ones per item.
### 5. Update standalone download routes
`download-document-certificate.ts` and `download-document-audit-logs.ts` also hardcode A4:
- Both routes have `documentId` which maps to a specific envelope item
- Fetch **that specific document's** PDF data, load it, read last page width + height via `getLastPageDimensions`
- Pass `{ pageWidth, pageHeight }` to the generator
- This ensures the standalone download matches the dimensions the user would see in the sealed PDF for that document
### 6. Edge cases
| Scenario | Behavior |
| --------------------------------------- | ------------------------------------------------------------------------------------------- |
| Mixed page sizes within one PDF | Use last page's dimensions (per spec) |
| Page dimensions below minimum threshold | Fall back to A4 (595×842) |
| Landscape pages | width/height just swap roles; renderers adapt via `Math.min()` capping. No special handling |
| Fallback if page dims unreadable | Default to A4 (595×842) |
| Resealing | Dimensions read after `initialData` remap — correct source automatically |
| Playwright PDF path enabled | Remains A4 — out of scope, deprecated |
| Single-doc envelope (most common) | One generation, same perf as today |
| Multi-doc envelope, same page sizes | Dedup key matches → one generation |
| Multi-doc envelope, different sizes | One generation per unique size |
### 7. Tests
- Add assertion-based E2E test (no visual regression / reference images needed)
- Seal a Letter-size (612×792) PDF through the full flow
- Load the sealed output and assert all pages (document + cert + audit) have matching width/height
- Can be added to `envelope-alignment.spec.ts` or as a new focused test
## Implementation Steps
1. **Extend `get-page-size.ts`** — add `getLastPageDimensions(pdfDoc: PDF): { width: number; height: number }` using `@libpdf/core`, with `Math.round()` and minimum dimension enforcement
2. **In `seal-document.handler.ts`**:
a. After the resealing remap (line ~159), load each envelope item's PDF via `PDF.load()` and collect last page `{ width, height }` using `getLastPageDimensions`
b. Deduplicate by `"${width}x${height}"` key
c. Generate cert/audit PDFs per unique size (parallel via `Promise.all`)
d. In envelope item loop, look up matching cert/audit doc by size key
3. **Fix `download-document-certificate.ts`** — load the specific document's PDF, read last page dims via `getLastPageDimensions`, pass to generator
4. **Fix `download-document-audit-logs.ts`** — same as above, using the specific `documentId`'s PDF
5. **Add E2E test** — assertion-based test with a Letter-size document verifying all page dimensions match after sealing
@@ -116,11 +116,9 @@ export function AssistantConfirmationDialog({
{!isEditingNextSigner && (
<div>
<p className="text-muted-foreground text-sm">
<Trans>
The next recipient to sign this document will be{' '}
<span className="font-semibold">{form.watch('name')}</span> (
<span className="font-semibold">{form.watch('email')}</span>).
</Trans>
The next recipient to sign this document will be{' '}
<span className="font-semibold">{form.watch('name')}</span> (
<span className="font-semibold">{form.watch('email')}</span>).
</p>
<Button
@@ -258,10 +258,10 @@ export const EnvelopeDistributeDialog = ({
>
<TabsList className="w-full">
<TabsTrigger className="w-full" value={DocumentDistributionMethod.EMAIL}>
<Trans>Email</Trans>
Email
</TabsTrigger>
<TabsTrigger className="w-full" value={DocumentDistributionMethod.NONE}>
<Trans>None</Trans>
None
</TabsTrigger>
</TabsList>
</Tabs>
@@ -127,11 +127,7 @@ export const TeamCreateDialog = ({ trigger, onCreated, ...props }: TeamCreateDia
};
const mapTextToUrl = (text: string) => {
return text
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/\s+/g, '-');
return text.toLowerCase().replace(/\s+/g, '-');
};
const dialogState = useMemo(() => {
@@ -264,7 +260,7 @@ export const TeamCreateDialog = ({ trigger, onCreated, ...props }: TeamCreateDia
<Input className="bg-background" {...field} />
</FormControl>
{!form.formState.errors.teamUrl && (
<span className="text-xs font-normal text-foreground/50">
<span className="text-foreground/50 text-xs font-normal">
{field.value ? (
`${NEXT_PUBLIC_WEBAPP_URL()}/t/${field.value}`
) : (
@@ -292,7 +288,7 @@ export const TeamCreateDialog = ({ trigger, onCreated, ...props }: TeamCreateDia
/>
<label
className="ml-2 text-sm text-muted-foreground"
className="text-muted-foreground ml-2 text-sm"
htmlFor="inherit-members"
>
<Trans>Allow all organisation members to access this team</Trans>
@@ -1,4 +1,3 @@
import { Trans } from '@lingui/react/macro';
import { Loader } from 'lucide-react';
export const EmbedClientLoading = () => {
@@ -6,9 +5,7 @@ export const EmbedClientLoading = () => {
<div className="bg-background fixed left-0 top-0 z-[9999] flex h-full w-full items-center justify-center">
<Loader className="mr-2 h-4 w-4 animate-spin" />
<span>
<Trans>Loading...</Trans>
</span>
<span>Loading...</span>
</div>
);
};
@@ -499,9 +499,7 @@ export const EmbedDirectTemplateClientPage = ({
{!hidePoweredBy && (
<div className="fixed bottom-0 left-0 z-40 rounded-tr bg-primary px-2 py-1 text-xs font-medium text-primary-foreground opacity-60 hover:opacity-100">
<span>
<Trans>Powered by</Trans>
</span>
<span>Powered by</span>
<BrandingLogo className="ml-2 inline-block h-[14px]" />
</div>
)}
@@ -510,9 +510,7 @@ export const EmbedSignDocumentV1ClientPage = ({
{!hidePoweredBy && (
<div className="fixed bottom-0 left-0 z-40 rounded-tr bg-primary px-2 py-1 text-xs font-medium text-primary-foreground opacity-60 hover:opacity-100">
<span>
<Trans>Powered by</Trans>
</span>
<span>Powered by</span>
<BrandingLogo className="ml-2 inline-block h-[14px]" />
</div>
)}
@@ -1,7 +1,6 @@
import { useEffect, useLayoutEffect, useState } from 'react';
import { useLingui } from '@lingui/react';
import { EnvelopeType } from '@prisma/client';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
@@ -26,7 +25,7 @@ export const EmbedSignDocumentV2ClientPage = ({
}: EmbedSignDocumentV2ClientPageProps) => {
const { _ } = useLingui();
const { envelope, recipient, envelopeData, setFullName, setEmail, fullName } =
const { envelope, recipient, envelopeData, setFullName, fullName } =
useRequiredEnvelopeSigningContext();
const { isCompleted, isRejected, recipientSignature } = envelopeData;
@@ -36,7 +35,6 @@ export const EmbedSignDocumentV2ClientPage = ({
const [hasFinishedInit, setHasFinishedInit] = useState(false);
const [allowDocumentRejection, setAllowDocumentRejection] = useState(false);
const [isNameLocked, setIsNameLocked] = useState(false);
const [isEmailLocked, setIsEmailLocked] = useState(envelope.type === EnvelopeType.DOCUMENT);
const onDocumentCompleted = (data: {
token: string;
@@ -134,17 +132,6 @@ export const EmbedSignDocumentV2ClientPage = ({
// Since a recipient can be provided a name we can lock it without requiring
// a to be provided by the parent application, unlike direct templates.
setIsNameLocked(!!data.lockName);
if (envelope.type === EnvelopeType.TEMPLATE) {
if (!isCompleted && data.email) {
setEmail(data.email);
}
if (data.email) {
setIsEmailLocked(!!data.lockEmail);
}
}
setAllowDocumentRejection(!!data.allowDocumentRejection);
if (data.darkModeDisabled) {
@@ -226,7 +213,6 @@ export const EmbedSignDocumentV2ClientPage = ({
return (
<EmbedSigningProvider
isNameLocked={isNameLocked}
isEmailLocked={isEmailLocked}
hidePoweredBy={hidePoweredBy}
allowDocumentRejection={allowDocumentRejection}
onDocumentCompleted={onDocumentCompleted}
@@ -124,9 +124,7 @@ export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps
</div>
<p className="text-muted-foreground text-sm">
© {new Date().getFullYear()} Documenso, Inc.
<br />
<Trans>All rights reserved.</Trans>
© {new Date().getFullYear()} Documenso, Inc. <br /> All rights reserved.
</p>
</div>
</SheetContent>
@@ -118,9 +118,7 @@ export const BillingPlans = ({ plans }: BillingPlansProps) => {
{price.product.features && price.product.features.length > 0 && (
<div className="mt-4 text-muted-foreground">
<div className="text-sm font-medium">
<Trans>Includes:</Trans>
</div>
<div className="text-sm font-medium">Includes:</div>
<ul className="mt-1 divide-y text-sm">
{price.product.features.map((feature, index) => (
@@ -4,7 +4,6 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { Trans } from '@lingui/react/macro';
import { RecipientRole } from '@prisma/client';
import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
import { z } from 'zod';
import { AppError } from '@documenso/lib/errors/app-error';
@@ -28,6 +27,7 @@ import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-p
export type DocumentSigningAuth2FAProps = {
actionTarget?: 'FIELD' | 'DOCUMENT';
actionVerb?: string;
open: boolean;
onOpenChange: (value: boolean) => void;
onReauthFormSubmit: (values?: TRecipientActionAuth) => Promise<void> | void;
@@ -44,6 +44,7 @@ type T2FAAuthFormSchema = z.infer<typeof Z2FAAuthFormSchema>;
export const DocumentSigningAuth2FA = ({
actionTarget = 'FIELD',
actionVerb = 'sign',
onReauthFormSubmit,
open,
onOpenChange,
@@ -100,39 +101,14 @@ export const DocumentSigningAuth2FA = ({
<Alert variant="warning">
<AlertDescription>
<p>
{match({ role: recipient.role, actionTarget })
.with({ role: RecipientRole.SIGNER, actionTarget: 'FIELD' }, () => (
<Trans>You need to setup 2FA to sign this field.</Trans>
))
.with({ role: RecipientRole.SIGNER, actionTarget: 'DOCUMENT' }, () => (
<Trans>You need to setup 2FA to sign this document.</Trans>
))
.with({ role: RecipientRole.APPROVER, actionTarget: 'FIELD' }, () => (
<Trans>You need to setup 2FA to approve this field.</Trans>
))
.with({ role: RecipientRole.APPROVER, actionTarget: 'DOCUMENT' }, () => (
<Trans>You need to setup 2FA to approve this document.</Trans>
))
.with({ role: RecipientRole.VIEWER, actionTarget: 'FIELD' }, () => (
<Trans>You need to setup 2FA to view this field.</Trans>
))
.with({ role: RecipientRole.VIEWER, actionTarget: 'DOCUMENT' }, () => (
<Trans>You need to setup 2FA to mark this document as viewed.</Trans>
))
.with({ role: RecipientRole.CC, actionTarget: 'FIELD' }, () => (
<Trans>You need to setup 2FA to view this field.</Trans>
))
.with({ role: RecipientRole.CC, actionTarget: 'DOCUMENT' }, () => (
<Trans>You need to setup 2FA to view this document.</Trans>
))
.with({ role: RecipientRole.ASSISTANT, actionTarget: 'FIELD' }, () => (
<Trans>You need to setup 2FA to assist with this field.</Trans>
))
.with({ role: RecipientRole.ASSISTANT, actionTarget: 'DOCUMENT' }, () => (
<Trans>You need to setup 2FA to assist with this document.</Trans>
))
.exhaustive()}
{recipient.role === RecipientRole.VIEWER && actionTarget === 'DOCUMENT' ? (
<Trans>You need to setup 2FA to mark this document as viewed.</Trans>
) : (
// Todo: Translate
`You need to setup 2FA to ${actionVerb.toLowerCase()} this ${actionTarget.toLowerCase()}.`
)}
</p>
<p className="mt-2">
<Trans>
By enabling 2FA, you will be required to enter a code from your authenticator app
@@ -162,9 +138,7 @@ export const DocumentSigningAuth2FA = ({
name="token"
render={({ field }) => (
<FormItem>
<FormLabel required>
<Trans>2FA token</Trans>
</FormLabel>
<FormLabel required>2FA token</FormLabel>
<FormControl>
<PinInput {...field} value={field.value ?? ''} maxLength={6}>
@@ -2,7 +2,6 @@ import { useState } from 'react';
import { Trans, useLingui } from '@lingui/react/macro';
import { RecipientRole } from '@prisma/client';
import { match } from 'ts-pattern';
import { authClient } from '@documenso/auth/client';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
@@ -14,11 +13,13 @@ import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-p
export type DocumentSigningAuthAccountProps = {
actionTarget?: 'FIELD' | 'DOCUMENT';
actionVerb?: string;
onOpenChange: (value: boolean) => void;
};
export const DocumentSigningAuthAccount = ({
actionTarget = 'FIELD',
actionVerb = 'sign',
onOpenChange,
}: DocumentSigningAuthAccountProps) => {
const { recipient, isDirectTemplate } = useRequiredDocumentSigningAuthContext();
@@ -54,110 +55,32 @@ export const DocumentSigningAuthAccount = ({
<fieldset disabled={isSigningOut} className="space-y-4">
<Alert variant="warning">
<AlertDescription>
<span>
{match({ role: recipient.role, actionTarget })
.with({ role: RecipientRole.SIGNER, actionTarget: 'FIELD' }, () =>
isDirectTemplate ? (
<Trans>To sign this field, you need to be logged in.</Trans>
) : (
<Trans>
To sign this field, you need to be logged in as{' '}
<strong>{recipient.email}</strong>
</Trans>
),
)
.with({ role: RecipientRole.SIGNER, actionTarget: 'DOCUMENT' }, () =>
isDirectTemplate ? (
<Trans>To sign this document, you need to be logged in.</Trans>
) : (
<Trans>
To sign this document, you need to be logged in as{' '}
<strong>{recipient.email}</strong>
</Trans>
),
)
.with({ role: RecipientRole.APPROVER, actionTarget: 'FIELD' }, () =>
isDirectTemplate ? (
<Trans>To approve this field, you need to be logged in.</Trans>
) : (
<Trans>
To approve this field, you need to be logged in as{' '}
<strong>{recipient.email}</strong>
</Trans>
),
)
.with({ role: RecipientRole.APPROVER, actionTarget: 'DOCUMENT' }, () =>
isDirectTemplate ? (
<Trans>To approve this document, you need to be logged in.</Trans>
) : (
<Trans>
To approve this document, you need to be logged in as{' '}
<strong>{recipient.email}</strong>
</Trans>
),
)
.with({ role: RecipientRole.VIEWER, actionTarget: 'FIELD' }, () =>
isDirectTemplate ? (
<Trans>To view this field, you need to be logged in.</Trans>
) : (
<Trans>
To view this field, you need to be logged in as{' '}
<strong>{recipient.email}</strong>
</Trans>
),
)
.with({ role: RecipientRole.VIEWER, actionTarget: 'DOCUMENT' }, () =>
isDirectTemplate ? (
<Trans>To mark this document as viewed, you need to be logged in.</Trans>
) : (
<Trans>
To mark this document as viewed, you need to be logged in as{' '}
<strong>{recipient.email}</strong>
</Trans>
),
)
.with({ role: RecipientRole.CC, actionTarget: 'FIELD' }, () =>
isDirectTemplate ? (
<Trans>To view this field, you need to be logged in.</Trans>
) : (
<Trans>
To view this field, you need to be logged in as{' '}
<strong>{recipient.email}</strong>
</Trans>
),
)
.with({ role: RecipientRole.CC, actionTarget: 'DOCUMENT' }, () =>
isDirectTemplate ? (
<Trans>To view this document, you need to be logged in.</Trans>
) : (
<Trans>
To view this document, you need to be logged in as{' '}
<strong>{recipient.email}</strong>
</Trans>
),
)
.with({ role: RecipientRole.ASSISTANT, actionTarget: 'FIELD' }, () =>
isDirectTemplate ? (
<Trans>To assist with this field, you need to be logged in.</Trans>
) : (
<Trans>
To assist with this field, you need to be logged in as{' '}
<strong>{recipient.email}</strong>
</Trans>
),
)
.with({ role: RecipientRole.ASSISTANT, actionTarget: 'DOCUMENT' }, () =>
isDirectTemplate ? (
<Trans>To assist with this document, you need to be logged in.</Trans>
) : (
<Trans>
To assist with this document, you need to be logged in as{' '}
<strong>{recipient.email}</strong>
</Trans>
),
)
.exhaustive()}
</span>
{actionTarget === 'DOCUMENT' && recipient.role === RecipientRole.VIEWER ? (
<span>
{isDirectTemplate ? (
<Trans>To mark this document as viewed, you need to be logged in.</Trans>
) : (
<Trans>
To mark this document as viewed, you need to be logged in as{' '}
<strong>{recipient.email}</strong>
</Trans>
)}
</span>
) : (
<span>
{isDirectTemplate ? (
<Trans>
To {actionVerb.toLowerCase()} this {actionTarget.toLowerCase()}, you need to be
logged in.
</Trans>
) : (
<Trans>
To {actionVerb.toLowerCase()} this {actionTarget.toLowerCase()}, you need to be
logged in as <strong>{recipient.email}</strong>
</Trans>
)}
</span>
)}
</AlertDescription>
</Alert>
@@ -8,7 +8,6 @@ import { RecipientRole } from '@prisma/client';
import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser';
import { Loader } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
import { z } from 'zod';
import { AppError } from '@documenso/lib/errors/app-error';
@@ -39,6 +38,7 @@ import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-p
export type DocumentSigningAuthPasskeyProps = {
actionTarget?: 'FIELD' | 'DOCUMENT';
actionVerb?: string;
open: boolean;
onOpenChange: (value: boolean) => void;
onReauthFormSubmit: (values?: TRecipientActionAuth) => Promise<void> | void;
@@ -52,6 +52,7 @@ type TPasskeyAuthFormSchema = z.infer<typeof ZPasskeyAuthFormSchema>;
export const DocumentSigningAuthPasskey = ({
actionTarget = 'FIELD',
actionVerb = 'sign',
onReauthFormSubmit,
open,
onOpenChange,
@@ -127,62 +128,9 @@ export const DocumentSigningAuthPasskey = ({
<div className="space-y-4">
<Alert variant="warning">
<AlertDescription>
{match({ role: recipient.role, actionTarget })
.with({ role: RecipientRole.SIGNER, actionTarget: 'FIELD' }, () => (
<Trans>
Your browser does not support passkeys, which is required to sign this field.
</Trans>
))
.with({ role: RecipientRole.SIGNER, actionTarget: 'DOCUMENT' }, () => (
<Trans>
Your browser does not support passkeys, which is required to sign this document.
</Trans>
))
.with({ role: RecipientRole.APPROVER, actionTarget: 'FIELD' }, () => (
<Trans>
Your browser does not support passkeys, which is required to approve this field.
</Trans>
))
.with({ role: RecipientRole.APPROVER, actionTarget: 'DOCUMENT' }, () => (
<Trans>
Your browser does not support passkeys, which is required to approve this
document.
</Trans>
))
.with({ role: RecipientRole.VIEWER, actionTarget: 'FIELD' }, () => (
<Trans>
Your browser does not support passkeys, which is required to view this field.
</Trans>
))
.with({ role: RecipientRole.VIEWER, actionTarget: 'DOCUMENT' }, () => (
<Trans>
Your browser does not support passkeys, which is required to mark this document as
viewed.
</Trans>
))
.with({ role: RecipientRole.CC, actionTarget: 'FIELD' }, () => (
<Trans>
Your browser does not support passkeys, which is required to view this field.
</Trans>
))
.with({ role: RecipientRole.CC, actionTarget: 'DOCUMENT' }, () => (
<Trans>
Your browser does not support passkeys, which is required to view this document.
</Trans>
))
.with({ role: RecipientRole.ASSISTANT, actionTarget: 'FIELD' }, () => (
<Trans>
Your browser does not support passkeys, which is required to assist with this
field.
</Trans>
))
.with({ role: RecipientRole.ASSISTANT, actionTarget: 'DOCUMENT' }, () => (
<Trans>
Your browser does not support passkeys, which is required to assist with this
document.
</Trans>
))
.exhaustive()}
{/* Todo: Translate */}
Your browser does not support passkeys, which is required to {actionVerb.toLowerCase()}{' '}
this {actionTarget.toLowerCase()}.
</AlertDescription>
</Alert>
@@ -230,38 +178,10 @@ export const DocumentSigningAuthPasskey = ({
<div className="space-y-4">
<Alert variant="warning">
<AlertDescription>
{match({ role: recipient.role, actionTarget })
.with({ role: RecipientRole.SIGNER, actionTarget: 'FIELD' }, () => (
<Trans>You need to setup a passkey to sign this field.</Trans>
))
.with({ role: RecipientRole.SIGNER, actionTarget: 'DOCUMENT' }, () => (
<Trans>You need to setup a passkey to sign this document.</Trans>
))
.with({ role: RecipientRole.APPROVER, actionTarget: 'FIELD' }, () => (
<Trans>You need to setup a passkey to approve this field.</Trans>
))
.with({ role: RecipientRole.APPROVER, actionTarget: 'DOCUMENT' }, () => (
<Trans>You need to setup a passkey to approve this document.</Trans>
))
.with({ role: RecipientRole.VIEWER, actionTarget: 'FIELD' }, () => (
<Trans>You need to setup a passkey to view this field.</Trans>
))
.with({ role: RecipientRole.VIEWER, actionTarget: 'DOCUMENT' }, () => (
<Trans>You need to setup a passkey to mark this document as viewed.</Trans>
))
.with({ role: RecipientRole.CC, actionTarget: 'FIELD' }, () => (
<Trans>You need to setup a passkey to view this field.</Trans>
))
.with({ role: RecipientRole.CC, actionTarget: 'DOCUMENT' }, () => (
<Trans>You need to setup a passkey to view this document.</Trans>
))
.with({ role: RecipientRole.ASSISTANT, actionTarget: 'FIELD' }, () => (
<Trans>You need to setup a passkey to assist with this field.</Trans>
))
.with({ role: RecipientRole.ASSISTANT, actionTarget: 'DOCUMENT' }, () => (
<Trans>You need to setup a passkey to assist with this document.</Trans>
))
.exhaustive()}
{/* Todo: Translate */}
{recipient.role === RecipientRole.VIEWER && actionTarget === 'DOCUMENT'
? 'You need to setup a passkey to mark this document as viewed.'
: `You need to setup a passkey to ${actionVerb.toLowerCase()} this ${actionTarget.toLowerCase()}.`}
</AlertDescription>
</Alert>
@@ -293,9 +213,7 @@ export const DocumentSigningAuthPasskey = ({
name="passkeyId"
render={({ field }) => (
<FormItem>
<FormLabel required>
<Trans>Passkey</Trans>
</FormLabel>
<FormLabel required>Passkey</FormLabel>
<FormControl>
<Select {...field} onValueChange={field.onChange}>
@@ -323,24 +241,20 @@ export const DocumentSigningAuthPasskey = ({
{formErrorCode && (
<Alert variant="destructive">
<AlertTitle>
<Trans>Unauthorized</Trans>
</AlertTitle>
<AlertTitle>Unauthorized</AlertTitle>
<AlertDescription>
<Trans>
We were unable to verify your details. Please try again or contact support
</Trans>
We were unable to verify your details. Please try again or contact support
</AlertDescription>
</Alert>
)}
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
<Trans>Cancel</Trans>
Cancel
</Button>
<Button type="submit" loading={isCurrentlyAuthenticating}>
<Trans>Sign</Trans>
Sign
</Button>
</DialogFooter>
</div>
@@ -23,6 +23,8 @@ import { Input } from '@documenso/ui/primitives/input';
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
export type DocumentSigningAuthPasswordProps = {
actionTarget?: 'FIELD' | 'DOCUMENT';
actionVerb?: string;
open: boolean;
onOpenChange: (value: boolean) => void;
onReauthFormSubmit: (values?: TRecipientActionAuth) => Promise<void> | void;
@@ -38,6 +40,8 @@ const ZPasswordAuthFormSchema = z.object({
type TPasswordAuthFormSchema = z.infer<typeof ZPasswordAuthFormSchema>;
export const DocumentSigningAuthPassword = ({
actionTarget = 'FIELD',
actionVerb = 'sign',
onReauthFormSubmit,
open,
onOpenChange,
@@ -162,9 +162,7 @@ export const DocumentSigningAutoSign = ({ recipient, fields }: DocumentSigningAu
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans>Automatically sign fields</Trans>
</DialogTitle>
<DialogTitle>Automatically sign fields</DialogTitle>
</DialogHeader>
<div className="text-muted-foreground max-w-[50ch]">
@@ -122,9 +122,7 @@ export const DocumentSigningMobileWidget = () => {
{!hidePoweredBy && (
<div className="mt-2 inline-block rounded bg-primary px-2 py-1 text-xs font-medium text-primary-foreground opacity-60 hover:opacity-100 lg:hidden">
<span>
<Trans>Powered by</Trans>
</span>
<span>Powered by</span>
<BrandingLogo className="ml-2 inline-block h-[14px]" />
</div>
)}
@@ -252,9 +252,7 @@ export const DocumentSigningPageViewV2 = () => {
target="_blank"
className="fixed bottom-0 right-0 z-40 hidden cursor-pointer rounded-tl bg-primary px-2 py-1 text-xs font-medium text-primary-foreground opacity-60 hover:opacity-100 lg:block"
>
<span>
<Trans>Powered by</Trans>
</span>
<span>Powered by</span>
<BrandingLogo className="ml-2 inline-block h-[14px]" />
</a>
)}
@@ -434,31 +434,15 @@ export default function EnvelopeEditorFieldsPageRenderer() {
renderFieldOnLayer(field);
});
// Reconcile selection state with live field nodes after flush/sync updates.
const liveSelectedFieldGroups = selectedKonvaFieldGroups.filter((fieldGroup) => {
if (!fieldGroup.getStage() || !fieldGroup.getParent()) {
return false;
}
return localPageFields.some((field) => field.formId === fieldGroup.id());
});
if (liveSelectedFieldGroups.length !== selectedKonvaFieldGroups.length) {
setSelectedFields(liveSelectedFieldGroups);
}
// Rerender the transformer
interactiveTransformer.current?.forceUpdate();
pageLayer.current.batchDraw();
}, [localPageFields, selectedKonvaFieldGroups]);
}, [localPageFields]);
const setSelectedFields = (nodes: Konva.Node[]) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const fieldGroups = nodes.filter(
(node) =>
node.hasName('field-group') && Boolean(node.getStage()) && Boolean(node.getParent()),
) as Konva.Group[];
const fieldGroups = nodes.filter((node) => node.hasName('field-group')) as Konva.Group[];
interactiveTransformer.current?.nodes(fieldGroups);
setSelectedKonvaFieldGroups(fieldGroups);
@@ -690,10 +674,6 @@ const FieldActionButtons = ({
selectedFieldFormId.includes(field.formId),
);
if (fields.length === 0) {
return null;
}
const recipient = envelope.recipients.find(
(recipient) => recipient.id === fields[0].recipientId,
);
@@ -709,7 +689,7 @@ const FieldActionButtons = ({
}
return null;
}, [editorFields.localFields, envelope.recipients, selectedFieldFormId]);
}, [editorFields.localFields]);
return (
<div className="flex flex-col items-center" {...props}>
@@ -296,31 +296,19 @@ export const EnvelopeEditorFieldsPage = () => {
<div className="space-y-2 rounded-md border border-border bg-muted/50 p-3 text-sm text-foreground">
<p>
<span className="min-w-12 text-muted-foreground">
<Trans>Pos X:</Trans>
</span>
&nbsp;
<span className="min-w-12 text-muted-foreground">Pos X:&nbsp;</span>
{selectedField.positionX.toFixed(2)}
</p>
<p>
<span className="min-w-12 text-muted-foreground">
<Trans>Pos Y:</Trans>
</span>
&nbsp;
<span className="min-w-12 text-muted-foreground">Pos Y:&nbsp;</span>
{selectedField.positionY.toFixed(2)}
</p>
<p>
<span className="min-w-12 text-muted-foreground">
<Trans>Width:</Trans>
</span>
&nbsp;
<span className="min-w-12 text-muted-foreground">Width:&nbsp;</span>
{selectedField.width.toFixed(2)}
</p>
<p>
<span className="min-w-12 text-muted-foreground">
<Trans>Height:</Trans>
</span>
&nbsp;
<span className="min-w-12 text-muted-foreground">Height:&nbsp;</span>
{selectedField.height.toFixed(2)}
</p>
</div>
@@ -51,7 +51,7 @@ export const UserProfileSkeleton = ({ className, user, rows = 2 }: UserProfileSk
<div className="mt-8 w-full">
<div className="dark:divide-foreground/30 dark:border-foreground/30 divide-y-2 divide-neutral-200 overflow-hidden rounded-lg border-2 border-neutral-200">
<div className="text-muted-foreground dark:bg-foreground/20 bg-neutral-50 p-4 font-medium">
<Trans>Documents</Trans>
Documents
</div>
{Array(rows)
@@ -1,6 +1,5 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { WebhookTriggerEvents } from '@prisma/client';
import { toFriendlyWebhookEventName } from '@documenso/lib/universal/webhook/to-friendly-webhook-event-name';
@@ -42,11 +41,7 @@ export const WebhookMultiSelectCombobox = ({
placeholder={_(msg`Select triggers`)}
hideClearAllButton
hidePlaceholderWhenSelected
emptyIndicator={
<p className="text-center text-sm">
<Trans>No triggers available</Trans>
</p>
}
emptyIndicator={<p className="text-center text-sm">No triggers available</p>}
/>
);
};
@@ -96,9 +96,7 @@ export const AdminDocumentLogsTable = ({ envelopeId }: AdminDocumentLogsTablePro
)}
</div>
) : (
<p>
<Trans>N/A</Trans>
</p>
<p>N/A</p>
),
},
{
@@ -2,7 +2,6 @@ import { useMemo } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { DateTime } from 'luxon';
import type { DateTimeFormatOptions } from 'luxon';
import { useSearchParams } from 'react-router';
@@ -89,9 +88,7 @@ export const DocumentLogsTable = ({ documentId, userId }: DocumentLogsTableProps
)}
</div>
) : (
<p>
<Trans>N/A</Trans>
</p>
<p>N/A</p>
),
},
{
@@ -107,11 +107,7 @@ export default function OrganisationGroupSettingsPage({
cell: ({ row }) => (
<div className="flex items-center gap-2">
<Link to={`/admin/users/${row.original.user.id}`}>{row.original.user.name}</Link>
{row.original.user.id === organisation?.ownerUserId && (
<Badge>
<Trans>Owner</Trans>
</Badge>
)}
{row.original.user.id === organisation?.ownerUserId && <Badge>Owner</Badge>}
</div>
),
},
@@ -212,9 +208,7 @@ export default function OrganisationGroupSettingsPage({
{SUBSCRIPTION_STATUS_MAP[organisation.subscription.status]} subscription found
</span>
) : (
<span>
<Trans>No subscription found</Trans>
</span>
<span>No subscription found</span>
)}
</AlertDescription>
</div>
@@ -66,9 +66,7 @@ export default function DocumentsFoldersPage() {
onClick={() => navigateToFolder(null)}
>
<HomeIcon className="h-4 w-4" />
<span>
<Trans>Home</Trans>
</span>
<span>Home</span>
</Button>
</div>
@@ -66,9 +66,7 @@ export default function TemplatesFoldersPage() {
onClick={() => navigateToFolder(null)}
>
<HomeIcon className="h-4 w-4" />
<span>
<Trans>Home</Trans>
</span>
<span>Home</span>
</Button>
</div>
@@ -1,4 +1,3 @@
import { Trans } from '@lingui/react/macro';
import { Outlet, isRouteErrorResponse, useRouteError } from 'react-router';
import {
@@ -90,9 +89,5 @@ export function ErrorBoundary({ loaderData }: Route.ErrorBoundaryProps) {
}
}
return (
<div>
<Trans>Not Found</Trans>
</div>
);
return <div>Not Found</div>;
}
@@ -1,6 +1,5 @@
import { useLayoutEffect } from 'react';
import { Trans } from '@lingui/react/macro';
import { Outlet, useLoaderData } from 'react-router';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
@@ -76,11 +75,7 @@ export default function AuthoringLayout() {
}, []);
if (!hasValidToken) {
return (
<div>
<Trans>Invalid embedding presign token provided</Trans>
</div>
);
return <div>Invalid embedding presign token provided</div>;
}
return (
@@ -1,6 +1,5 @@
import { useEffect, useLayoutEffect, useState } from 'react';
import { Trans } from '@lingui/react/macro';
import { SigningStatus } from '@prisma/client';
import { useRevalidator } from 'react-router';
@@ -284,9 +283,7 @@ export default function MultisignPage() {
{!hidePoweredBy && (
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
<span>
<Trans>Powered by</Trans>
</span>
<span>Powered by</span>
<BrandingLogo className="ml-2 inline-block h-[14px]" />
</div>
)}
@@ -301,9 +298,7 @@ export default function MultisignPage() {
{!hidePoweredBy && (
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
<span>
<Trans>Powered by</Trans>
</span>
<span>Powered by</span>
<BrandingLogo className="ml-2 inline-block h-[14px]" />
</div>
)}
+1 -1
View File
@@ -106,5 +106,5 @@
"vite-plugin-babel-macros": "^1.0.6",
"vite-tsconfig-paths": "^5.1.4"
},
"version": "2.6.1"
"version": "2.6.0"
}
-5
View File
@@ -12,8 +12,6 @@ import { API_V2_BETA_URL, API_V2_URL } from '@documenso/lib/constants/app';
import { jobsClient } from '@documenso/lib/jobs/client';
import { LicenseClient } from '@documenso/lib/server-only/license/license-client';
import { TelemetryClient } from '@documenso/lib/server-only/telemetry/telemetry-client';
import { migrateDeletedAccountServiceAccount } from '@documenso/lib/server-only/user/service-accounts/deleted-account';
import { migrateLegacyServiceAccount } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account';
import { getIpAddress } from '@documenso/lib/universal/get-ip-address';
import { env } from '@documenso/lib/utils/env';
import { logger } from '@documenso/lib/utils/logger';
@@ -146,7 +144,4 @@ if (env('NODE_ENV') !== 'development') {
// Start license client to verify license on startup.
void LicenseClient.start();
void migrateDeletedAccountServiceAccount();
void migrateLegacyServiceAccount();
export default app;
-51
View File
@@ -1,51 +0,0 @@
%PDF-1.7
%âãÏÓ
1 0 obj
<<
/Type /Pages
/Kids [4 0 R]
/Count 1
>>
endobj
2 0 obj
<<
/Type /Catalog
/Pages 1 0 R
>>
endobj
3 0 obj
<<
/Title (Untitled)
/Author (Unknown)
/Creator (@libpdf/core)
/Producer (@libpdf/core)
/CreationDate (D:20260211083727Z)
/ModDate (D:20260211083727Z)
>>
endobj
4 0 obj
<<
/Type /Page
/MediaBox [0 0 595 842]
/Resources <<
>>
/Parent 1 0 R
>>
endobj
xref
0 5
0000000000 65535 f
0000000015 00000 n
0000000072 00000 n
0000000121 00000 n
0000000290 00000 n
trailer
<<
/Size 5
/Root 2 0 R
/Info 3 0 R
/ID [<B051F100F1EED01A592FC6119F589603> <B051F100F1EED01A592FC6119F589603>]
>>
startxref
378
%%EOF
-51
View File
@@ -1,51 +0,0 @@
%PDF-1.7
%âãÏÓ
1 0 obj
<<
/Type /Pages
/Kids [4 0 R]
/Count 1
>>
endobj
2 0 obj
<<
/Type /Catalog
/Pages 1 0 R
>>
endobj
3 0 obj
<<
/Title (Untitled)
/Author (Unknown)
/Creator (@libpdf/core)
/Producer (@libpdf/core)
/CreationDate (D:20260211081729Z)
/ModDate (D:20260211081729Z)
>>
endobj
4 0 obj
<<
/Type /Page
/MediaBox [0 0 612 792]
/Resources <<
>>
/Parent 1 0 R
>>
endobj
xref
0 5
0000000000 65535 f
0000000015 00000 n
0000000072 00000 n
0000000121 00000 n
0000000290 00000 n
trailer
<<
/Size 5
/Root 2 0 R
/Info 3 0 R
/ID [<94A5FB5DCF5A94AD8C472C493420962C> <94A5FB5DCF5A94AD8C472C493420962C>]
>>
startxref
378
%%EOF
-51
View File
@@ -1,51 +0,0 @@
%PDF-1.7
%âãÏÓ
1 0 obj
<<
/Type /Pages
/Kids [4 0 R]
/Count 1
>>
endobj
2 0 obj
<<
/Type /Catalog
/Pages 1 0 R
>>
endobj
3 0 obj
<<
/Title (Untitled)
/Author (Unknown)
/Creator (@libpdf/core)
/Producer (@libpdf/core)
/CreationDate (D:20260211084535Z)
/ModDate (D:20260211084535Z)
>>
endobj
4 0 obj
<<
/Type /Page
/MediaBox [0 0 1224 792]
/Resources <<
>>
/Parent 1 0 R
>>
endobj
xref
0 5
0000000000 65535 f
0000000015 00000 n
0000000072 00000 n
0000000121 00000 n
0000000290 00000 n
trailer
<<
/Size 5
/Root 2 0 R
/Info 3 0 R
/ID [<694452F2208AC8E3DD2D2488544F9F0C> <694452F2208AC8E3DD2D2488544F9F0C>]
>>
startxref
379
%%EOF
+3 -3
View File
@@ -1,12 +1,12 @@
{
"name": "@documenso/root",
"version": "2.6.1",
"version": "2.6.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@documenso/root",
"version": "2.6.1",
"version": "2.6.0",
"hasInstallScript": true,
"workspaces": [
"apps/*",
@@ -108,7 +108,7 @@
},
"apps/remix": {
"name": "@documenso/remix",
"version": "2.6.1",
"version": "2.6.0",
"dependencies": {
"@cantoo/pdf-lib": "^2.5.3",
"@documenso/api": "*",
+1 -1
View File
@@ -5,7 +5,7 @@
"apps/*",
"packages/*"
],
"version": "2.6.1",
"version": "2.6.0",
"scripts": {
"postinstall": "patch-package",
"build": "turbo run build",
@@ -106,10 +106,10 @@ test.describe('AutoSave Subject Step', () => {
const { user, document, team } = await setupDocumentAndNavigateToSubjectStep(page);
// Toggle some email settings checkboxes (randomly - some checked, some unchecked)
await page.getByText('Email the owner when a recipient signs').click();
await page.getByText("Email recipients when they're removed from a pending document").click();
await page.getByText('Email recipients when the document is completed', { exact: true }).click();
await page.getByText('Email recipients when a pending document is deleted').click();
await page.getByText('Send recipient signed email').click();
await page.getByText('Send recipient removed email').click();
await page.getByText('Send document completed email', { exact: true }).click();
await page.getByText('Send document deleted email').click();
await triggerAutosave(page);
@@ -126,30 +126,26 @@ test.describe('AutoSave Subject Step', () => {
const emailSettings = retrievedDocumentData.documentMeta?.emailSettings;
await expect(page.getByText('Email the owner when a recipient signs')).toBeChecked({
await expect(page.getByText('Send recipient signed email')).toBeChecked({
checked: emailSettings?.recipientSigned,
});
await expect(
page.getByText("Email recipients when they're removed from a pending document"),
).toBeChecked({
await expect(page.getByText('Send recipient removed email')).toBeChecked({
checked: emailSettings?.recipientRemoved,
});
await expect(
page.getByText('Email recipients when the document is completed', { exact: true }),
).toBeChecked({
await expect(page.getByText('Send document completed email', { exact: true })).toBeChecked({
checked: emailSettings?.documentCompleted,
});
await expect(page.getByText('Email recipients when a pending document is deleted')).toBeChecked({
await expect(page.getByText('Send document deleted email')).toBeChecked({
checked: emailSettings?.documentDeleted,
});
await expect(page.getByText('Email recipients with a signing request')).toBeChecked({
await expect(page.getByText('Send recipient signing request email')).toBeChecked({
checked: emailSettings?.recipientSigningRequest,
});
await expect(page.getByText('Email the signer if the document is still pending')).toBeChecked({
await expect(page.getByText('Send document pending email')).toBeChecked({
checked: emailSettings?.documentPending,
});
await expect(page.getByText('Email the owner when the document is completed')).toBeChecked({
await expect(page.getByText('Send document completed email to the owner')).toBeChecked({
checked: emailSettings?.ownerDocumentCompleted,
});
}).toPass();
@@ -165,10 +161,10 @@ test.describe('AutoSave Subject Step', () => {
await page.getByRole('textbox', { name: 'Subject (Optional)' }).fill(subject);
await page.getByRole('textbox', { name: 'Message (Optional)' }).fill(message);
await page.getByText('Email the owner when a recipient signs').click();
await page.getByText("Email recipients when they're removed from a pending document").click();
await page.getByText('Email recipients when the document is completed', { exact: true }).click();
await page.getByText('Email recipients when a pending document is deleted').click();
await page.getByText('Send recipient signed email').click();
await page.getByText('Send recipient removed email').click();
await page.getByText('Send document completed email', { exact: true }).click();
await page.getByText('Send document deleted email').click();
await triggerAutosave(page);
@@ -194,30 +190,26 @@ test.describe('AutoSave Subject Step', () => {
retrievedDocumentData.documentMeta?.message ?? '',
);
await expect(page.getByText('Email the owner when a recipient signs')).toBeChecked({
await expect(page.getByText('Send recipient signed email')).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.recipientSigned,
});
await expect(
page.getByText("Email recipients when they're removed from a pending document"),
).toBeChecked({
await expect(page.getByText('Send recipient removed email')).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.recipientRemoved,
});
await expect(
page.getByText('Email recipients when the document is completed', { exact: true }),
).toBeChecked({
await expect(page.getByText('Send document completed email', { exact: true })).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentCompleted,
});
await expect(page.getByText('Email recipients when a pending document is deleted')).toBeChecked({
await expect(page.getByText('Send document deleted email')).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentDeleted,
});
await expect(page.getByText('Email recipients with a signing request')).toBeChecked({
await expect(page.getByText('Send recipient signing request email')).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.recipientSigningRequest,
});
await expect(page.getByText('Email the signer if the document is still pending')).toBeChecked({
await expect(page.getByText('Send document pending email')).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentPending,
});
await expect(page.getByText('Email the owner when the document is completed')).toBeChecked({
await expect(page.getByText('Send document completed email to the owner')).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.ownerDocumentCompleted,
});
}).toPass();
@@ -1,218 +0,0 @@
import { type APIRequestContext, type Page, expect, test } from '@playwright/test';
import { DocumentStatus, EnvelopeType, FieldType } from '@prisma/client';
import fs from 'node:fs';
import path from 'node:path';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
import { prisma } from '@documenso/prisma';
import { seedUser } from '@documenso/prisma/seed/users';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../lib/constants/app';
import { createApiToken } from '../../../lib/server-only/public-api/create-api-token';
import { RecipientRole } from '../../../prisma/generated/types';
import type {
TCreateEnvelopePayload,
TCreateEnvelopeResponse,
} from '../../../trpc/server/envelope-router/create-envelope.types';
import type { TDistributeEnvelopeRequest } from '../../../trpc/server/envelope-router/distribute-envelope.types';
import { apiSignin } from '../fixtures/authentication';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
const baseUrl = `${WEBAPP_BASE_URL}/api/v2`;
test.describe.configure({ mode: 'parallel', timeout: 60000 });
const signAndVerifyPageDimensions = async ({
page,
request,
pdfFile,
identifier,
title,
expectedWidth,
expectedHeight,
}: {
page: Page;
request: APIRequestContext;
pdfFile: string;
identifier: string;
title: string;
expectedWidth: number;
expectedHeight: number;
}) => {
const { user, team } = await seedUser();
const { token } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const pdfBuffer = fs.readFileSync(path.join(__dirname, `../../../../assets/${pdfFile}`));
const formData = new FormData();
const createEnvelopePayload: TCreateEnvelopePayload = {
type: EnvelopeType.DOCUMENT,
title,
recipients: [
{
email: user.email,
name: user.name || '',
role: RecipientRole.SIGNER,
fields: [
{
identifier,
type: FieldType.SIGNATURE,
fieldMeta: { type: 'signature' },
page: 1,
positionX: 10,
positionY: 10,
width: 40,
height: 10,
},
],
},
],
};
formData.append('payload', JSON.stringify(createEnvelopePayload));
formData.append('files', new File([pdfBuffer], identifier, { type: 'application/pdf' }));
const createResponse = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
expect(createResponse.ok()).toBeTruthy();
const { id: envelopeId }: TCreateEnvelopeResponse = await createResponse.json();
const envelope = await prisma.envelope.findUniqueOrThrow({
where: { id: envelopeId },
include: { recipients: true },
});
const distributeResponse = await request.post(`${baseUrl}/envelope/distribute`, {
headers: { Authorization: `Bearer ${token}` },
data: { envelopeId: envelope.id } satisfies TDistributeEnvelopeRequest,
});
expect(distributeResponse.ok()).toBeTruthy();
// Pre-insert all fields via Prisma so we can skip the UI field interaction.
const fields = await prisma.field.findMany({
where: { envelopeId: envelope.id, inserted: false },
});
for (const field of fields) {
await prisma.field.update({
where: { id: field.id },
data: {
inserted: true,
signature: {
create: {
recipientId: envelope.recipients[0].id,
typedSignature: 'Test Signature',
},
},
},
});
}
const recipientToken = envelope.recipients[0].token;
const signUrl = `/sign/${recipientToken}`;
await apiSignin({
page,
email: user.email,
redirectPath: signUrl,
});
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(`${signUrl}/complete`);
await expect(async () => {
const { status } = await prisma.envelope.findFirstOrThrow({
where: { id: envelope.id },
});
expect(status).toBe(DocumentStatus.COMPLETED);
}).toPass({ timeout: 10000 });
const completedEnvelope = await prisma.envelope.findFirstOrThrow({
where: { id: envelope.id },
include: {
envelopeItems: {
orderBy: { order: 'asc' },
include: { documentData: true },
},
},
});
for (const item of completedEnvelope.envelopeItems) {
const documentUrl = getEnvelopeItemPdfUrl({
type: 'download',
envelopeItem: item,
token: recipientToken,
version: 'signed',
});
const pdfData = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
const loadingTask = pdfjsLib.getDocument({ data: new Uint8Array(pdfData) });
const pdf = await loadingTask.promise;
expect(pdf.numPages).toBeGreaterThan(1);
for (let i = 1; i <= pdf.numPages; i++) {
const pdfPage = await pdf.getPage(i);
const viewport = pdfPage.getViewport({ scale: 1 });
expect(Math.round(viewport.width)).toBe(expectedWidth);
expect(Math.round(viewport.height)).toBe(expectedHeight);
}
}
};
test('cert and audit log pages match letter page dimensions', async ({ page, request }) => {
await signAndVerifyPageDimensions({
page,
request,
pdfFile: 'letter-size.pdf',
identifier: 'letter-doc',
title: 'Letter Size Dimension Test',
expectedWidth: 612,
expectedHeight: 792,
});
});
test('cert and audit log pages match A4 page dimensions', async ({ page, request }) => {
await signAndVerifyPageDimensions({
page,
request,
pdfFile: 'a4-size.pdf',
identifier: 'a4-doc',
title: 'A4 Size Dimension Test',
expectedWidth: 595,
expectedHeight: 842,
});
});
test('cert and audit log pages match tabloid landscape page dimensions', async ({
page,
request,
}) => {
await signAndVerifyPageDimensions({
page,
request,
pdfFile: 'tabloid-landscape.pdf',
identifier: 'tabloid-doc',
title: 'Tabloid Landscape Dimension Test',
expectedWidth: 1224,
expectedHeight: 792,
});
});
@@ -298,34 +298,18 @@ test('field placement visual regression', async ({ page, request }, testInfo) =>
*
* DON'T COMMIT THIS WITHOUT THE "SKIP" COMMAND.
*/
test.skip('download envelope images', async ({ page, request }) => {
test.skip('download envelope images', async ({ page }) => {
const { user, team } = await seedUser();
const { token: apiToken } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'test',
expiresIn: null,
});
const envelope = await seedAlignmentTestDocument({
userId: user.id,
teamId: team.id,
recipientName: user.name || '',
recipientEmail: user.email,
insertFields: true,
status: DocumentStatus.DRAFT,
status: DocumentStatus.PENDING,
});
const distributeEnvelopeRequest = await request.post(`${baseUrl}/envelope/distribute`, {
headers: { Authorization: `Bearer ${apiToken}` },
data: {
envelopeId: envelope.id,
} satisfies TDistributeEnvelopeRequest,
});
expect(distributeEnvelopeRequest.ok()).toBeTruthy();
const token = envelope.recipients[0].token;
const signUrl = `/sign/${token}`;
@@ -205,13 +205,9 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
await page.getByRole('textbox', { name: 'Reply to email' }).fill('organisation@documenso.com');
// Update email document settings by enabling/disabling some checkboxes
await page.getByRole('checkbox', { name: 'Email the owner when a recipient signs' }).uncheck();
await page
.getByRole('checkbox', { name: 'Email the signer if the document is still pending' })
.uncheck();
await page
.getByRole('checkbox', { name: 'Email recipients when a pending document is deleted' })
.uncheck();
await page.getByRole('checkbox', { name: 'Send recipient signed email' }).uncheck();
await page.getByRole('checkbox', { name: 'Send document pending email' }).uncheck();
await page.getByRole('checkbox', { name: 'Send document deleted email' }).uncheck();
await page.getByRole('button', { name: 'Update' }).first().click();
await expect(page.getByText('Your email preferences have been updated').first()).toBeVisible();
@@ -244,14 +240,12 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
await page.getByRole('option', { name: 'Override organisation settings' }).click();
// Update some email settings
await page.getByRole('checkbox', { name: 'Send recipient signing request email' }).uncheck();
await page
.getByRole('checkbox', { name: 'Email recipients with a signing request' })
.getByRole('checkbox', { name: 'Send document completed email', exact: true })
.uncheck();
await page
.getByRole('checkbox', { name: 'Email recipients when the document is completed', exact: true })
.uncheck();
await page
.getByRole('checkbox', { name: 'Email the owner when the document is completed' })
.getByRole('checkbox', { name: 'Send document completed email to the owner' })
.uncheck();
await page.getByRole('button', { name: 'Update' }).first().click();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 117 KiB

@@ -5,8 +5,6 @@ import { deleteCookie } from 'hono/cookie';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user';
import { deletedServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/deleted-account';
import { legacyServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account';
import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
import { prisma } from '@documenso/prisma';
@@ -28,13 +26,6 @@ export const handleOAuthCallbackUrl = async (options: HandleOAuthCallbackUrlOpti
const { email, name, sub, accessToken, accessTokenExpiresAt, idToken, redirectPath } =
await validateOauth({ c, clientOptions });
if (
email.toLowerCase() === legacyServiceAccountEmail() ||
email.toLowerCase() === deletedServiceAccountEmail()
) {
return c.text('FORBIDDEN', 403);
}
// Find the account if possible.
const existingAccount = await prisma.account.findFirst({
where: {
@@ -17,10 +17,7 @@ import { viewBackupCodes } from '@documenso/lib/server-only/2fa/view-backup-code
import { createUser } from '@documenso/lib/server-only/user/create-user';
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
import { getMostRecentEmailVerificationToken } from '@documenso/lib/server-only/user/get-most-recent-email-verification-token';
import { getUserByResetToken } from '@documenso/lib/server-only/user/get-user-by-reset-token';
import { resetPassword } from '@documenso/lib/server-only/user/reset-password';
import { deletedServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/deleted-account';
import { legacyServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account';
import { updatePassword } from '@documenso/lib/server-only/user/update-password';
import { verifyEmail } from '@documenso/lib/server-only/user/verify-email';
import { env } from '@documenso/lib/utils/env';
@@ -60,13 +57,6 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
});
}
if (
email.toLowerCase() === legacyServiceAccountEmail() ||
email.toLowerCase() === deletedServiceAccountEmail()
) {
return c.text('FORBIDDEN', 403);
}
const user = await prisma.user.findFirst({
where: {
email: email.toLowerCase(),
@@ -251,13 +241,6 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
.post('/forgot-password', sValidator('json', ZForgotPasswordSchema), async (c) => {
const { email } = c.req.valid('json');
if (
email.toLowerCase() === legacyServiceAccountEmail() ||
email.toLowerCase() === deletedServiceAccountEmail()
) {
return c.text('FORBIDDEN', 403);
}
await forgotPassword({
email,
});
@@ -270,15 +253,6 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
.post('/reset-password', sValidator('json', ZResetPasswordSchema), async (c) => {
const { token, password } = c.req.valid('json');
const user = await getUserByResetToken({ token });
if (
user.email.toLowerCase() === legacyServiceAccountEmail() ||
user.email.toLowerCase() === deletedServiceAccountEmail()
) {
return c.text('FORBIDDEN', 403);
}
const requestMetadata = c.get('requestMetadata');
const { userId } = await resetPassword({
-9
View File
@@ -5,8 +5,6 @@ import { isoBase64URL } from '@simplewebauthn/server/helpers';
import { Hono } from 'hono';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { deletedServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/deleted-account';
import { legacyServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account';
import type { TAuthenticationResponseJSONSchema } from '@documenso/lib/types/webauthn';
import { ZAuthenticationResponseJSONSchema } from '@documenso/lib/types/webauthn';
import { getAuthenticatorOptions } from '@documenso/lib/utils/authenticator';
@@ -76,13 +74,6 @@ export const passkeyRoute = new Hono<HonoAuthContext>()
const user = passkey.user;
if (
user.email.toLowerCase() === legacyServiceAccountEmail() ||
user.email.toLowerCase() === deletedServiceAccountEmail()
) {
return c.text('FORBIDDEN', 403);
}
const { rpId, origin } = getAuthenticatorOptions();
const verification = await verifyAuthenticationResponse({
@@ -1,89 +1,19 @@
import { SubscriptionStatus } from '@prisma/client';
import { createOrganisationClaimUpsertData } from '@documenso/lib/server-only/organisation/create-organisation';
import type { Stripe } from '@documenso/lib/server-only/stripe';
import { INTERNAL_CLAIM_ID, internalClaims } from '@documenso/lib/types/subscription';
import { prisma } from '@documenso/prisma';
import { extractStripeClaimId } from './on-subscription-updated';
export type OnSubscriptionDeletedOptions = {
subscription: Stripe.Subscription;
};
export const onSubscriptionDeleted = async ({ subscription }: OnSubscriptionDeletedOptions) => {
const existingSubscription = await prisma.subscription.findUnique({
where: {
planId: subscription.id,
},
include: {
organisation: {
include: {
organisationClaim: true,
},
},
},
});
// If the subscription doesn't exist, we don't need to do anything.
if (!existingSubscription) {
return;
}
const subscriptionClaimId = await extractClaimIdFromStripeSubscription(subscription);
// Individuals get their subscription deleted so they can return to the
// free plan.
if (subscriptionClaimId === INTERNAL_CLAIM_ID.INDIVIDUAL) {
await prisma.$transaction(async (tx) => {
await tx.subscription.delete({
where: {
id: existingSubscription.id,
},
});
await tx.organisationClaim.update({
where: {
id: existingSubscription.organisation.organisationClaim.id,
},
data: {
originalSubscriptionClaimId: INTERNAL_CLAIM_ID.FREE,
...createOrganisationClaimUpsertData(internalClaims[INTERNAL_CLAIM_ID.FREE]),
},
});
});
return;
}
// For all other cases, mark the subscription as inactive since
// they should still have a "Personal" account.
await prisma.subscription.update({
where: {
id: existingSubscription.id,
planId: subscription.id,
},
data: {
status: SubscriptionStatus.INACTIVE,
},
});
};
/**
* Extracts the claim ID from the Stripe subscription.
*
* Returns `null` if no claim ID found.
*/
const extractClaimIdFromStripeSubscription = async (subscription: Stripe.Subscription) => {
const deletedItem = subscription.items.data[0];
if (!deletedItem) {
return null;
}
try {
return await extractStripeClaimId(deletedItem.price);
} catch (error) {
console.error(error);
return null;
}
};
+2
View File
@@ -8,6 +8,8 @@ export const DOCUMENSO_INTERNAL_EMAIL = {
address: FROM_ADDRESS,
};
export const SERVICE_USER_EMAIL = 'serviceaccount@documenso.com';
export const EMAIL_VERIFICATION_STATE = {
NOT_FOUND: 'NOT_FOUND',
VERIFIED: 'VERIFIED',
@@ -15,11 +15,11 @@ import { groupBy } from 'remeda';
import { addRejectionStampToPdf } from '@documenso/lib/server-only/pdf/add-rejection-stamp-to-pdf';
import { generateAuditLogPdf } from '@documenso/lib/server-only/pdf/generate-audit-log-pdf';
import { generateCertificatePdf } from '@documenso/lib/server-only/pdf/generate-certificate-pdf';
import { getLastPageDimensions } from '@documenso/lib/server-only/pdf/get-page-size';
import { prisma } from '@documenso/prisma';
import { signPdf } from '@documenso/signing';
import { NEXT_PRIVATE_USE_PLAYWRIGHT_PDF } from '../../../constants/app';
import { PDF_SIZE_A4_72PPI } from '../../../constants/pdf';
import { AppError, AppErrorCode } from '../../../errors/app-error';
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
import { getAuditLogsPdf } from '../../../server-only/htmltopdf/get-audit-logs-pdf';
@@ -29,10 +29,7 @@ import { insertFieldInPDFV2 } from '../../../server-only/pdf/insert-field-in-pdf
import { legacy_insertFieldInPDF } from '../../../server-only/pdf/legacy-insert-field-in-pdf';
import { getTeamSettings } from '../../../server-only/team/get-team-settings';
import { triggerWebhook } from '../../../server-only/webhooks/trigger/trigger-webhook';
import {
DOCUMENT_AUDIT_LOG_TYPE,
type TDocumentAuditLog,
} from '../../../types/document-audit-logs';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
import {
ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload,
@@ -172,7 +169,6 @@ export const run = async ({
});
}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const envelopeCompletedAuditLog = createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED,
envelopeId: envelope.id,
@@ -186,24 +182,60 @@ export const run = async ({
const finalEnvelopeStatus = isRejected ? DocumentStatus.REJECTED : DocumentStatus.COMPLETED;
// Pre-fetch all PDF data so we can read dimensions and pass it
// to decorateAndSignPdf without fetching again.
const prefetchedItems = await Promise.all(
envelopeItems.map(async (envelopeItem) => {
const pdfData = await getFileServerSide(envelopeItem.documentData);
let certificateDoc: PDF | null = null;
let auditLogDoc: PDF | null = null;
return { envelopeItem, pdfData };
}),
);
if (settings.includeSigningCertificate || settings.includeAuditLog) {
const certificatePayload = {
envelope: {
...envelope,
status: finalEnvelopeStatus,
},
recipients: envelope.recipients, // Need to use the recipients from envelope which contains ALL recipients.
fields,
language: envelope.documentMeta.language,
envelopeOwner: {
email: envelope.user.email,
name: envelope.user.name || '',
},
envelopeItems: envelopeItems.map((item) => item.title),
pageWidth: PDF_SIZE_A4_72PPI.width,
pageHeight: PDF_SIZE_A4_72PPI.height,
envelopeCompletedAuditLog,
};
const usePlaywrightPdf = NEXT_PRIVATE_USE_PLAYWRIGHT_PDF();
// Use Playwright-based PDF generation if enabled, otherwise use Konva-based generation.
// This is a temporary toggle while we validate the Konva-based approach.
const usePlaywrightPdf = NEXT_PRIVATE_USE_PLAYWRIGHT_PDF();
const needsCertificate = settings.includeSigningCertificate;
const needsAuditLog = settings.includeAuditLog;
const makeCertificatePdf = async () =>
usePlaywrightPdf
? getCertificatePdf({
documentId,
language: envelope.documentMeta.language,
}).then(async (buffer) => PDF.load(buffer))
: generateCertificatePdf(certificatePayload);
const makeAuditLogPdf = async () =>
usePlaywrightPdf
? getAuditLogsPdf({
documentId,
language: envelope.documentMeta.language,
}).then(async (buffer) => PDF.load(buffer))
: generateAuditLogPdf(certificatePayload);
const [createdCertificatePdf, createdAuditLogPdf] = await Promise.all([
settings.includeSigningCertificate ? makeCertificatePdf() : null,
settings.includeAuditLog ? makeAuditLogPdf() : null,
]);
certificateDoc = createdCertificatePdf;
auditLogDoc = createdAuditLogPdf;
}
const newDocumentData: Array<{ oldDocumentDataId: string; newDocumentDataId: string }> = [];
for (const { envelopeItem, pdfData } of prefetchedItems) {
for (const envelopeItem of envelopeItems) {
const envelopeItemFields = envelope.envelopeItems.find(
(item) => item.id === envelopeItem.id,
)?.field;
@@ -212,70 +244,12 @@ export const run = async ({
throw new Error(`Envelope item fields not found for envelope item ${envelopeItem.id}`);
}
let certificateDoc: PDF | null = null;
let auditLogDoc: PDF | null = null;
if (needsCertificate || needsAuditLog) {
const pdfDoc = await PDF.load(pdfData);
const { width: pageWidth, height: pageHeight } = getLastPageDimensions(pdfDoc);
const additionalAuditLogs = [
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
{
...envelopeCompletedAuditLog,
id: '',
createdAt: new Date(),
} as TDocumentAuditLog,
];
const certificatePayload = {
envelope: {
...envelope,
status: finalEnvelopeStatus,
},
recipients: envelope.recipients,
fields,
language: envelope.documentMeta.language,
envelopeOwner: {
email: envelope.user.email,
name: envelope.user.name || '',
},
envelopeItems: envelopeItems.map((item) => item.title),
pageWidth,
pageHeight,
additionalAuditLogs,
};
const makeCertificatePdf = async () =>
usePlaywrightPdf
? getCertificatePdf({
documentId,
language: envelope.documentMeta.language,
}).then(async (buffer) => PDF.load(buffer))
: generateCertificatePdf(certificatePayload);
const makeAuditLogPdf = async () =>
usePlaywrightPdf
? getAuditLogsPdf({
documentId,
language: envelope.documentMeta.language,
}).then(async (buffer) => PDF.load(buffer))
: generateAuditLogPdf(certificatePayload);
[certificateDoc, auditLogDoc] = await Promise.all([
needsCertificate ? makeCertificatePdf() : null,
needsAuditLog ? makeAuditLogPdf() : null,
]);
}
const result = await decorateAndSignPdf({
envelope,
envelopeItem,
envelopeItemFields,
isRejected,
rejectionReason,
pdfData,
certificateDoc,
auditLogDoc,
});
@@ -364,13 +338,12 @@ type DecorateAndSignPdfOptions = {
envelopeItemFields: Field[];
isRejected: boolean;
rejectionReason: string;
pdfData: Uint8Array;
certificateDoc: PDF | null;
auditLogDoc: PDF | null;
};
/**
* Normalize, flatten and insert fields into a PDF document.
* Fetch, normalize, flatten and insert fields into a PDF document.
*/
const decorateAndSignPdf = async ({
envelope,
@@ -378,10 +351,11 @@ const decorateAndSignPdf = async ({
envelopeItemFields,
isRejected,
rejectionReason,
pdfData,
certificateDoc,
auditLogDoc,
}: DecorateAndSignPdfOptions) => {
const pdfData = await getFileServerSide(envelopeItem.documentData);
let pdfDoc = await PDF.load(pdfData);
// Normalize and flatten layers that could cause issues with the signature
@@ -1,7 +1,7 @@
import { PDF, rgb } from '@libpdf/core';
import type { FieldType, Recipient } from '@prisma/client';
import { type TFieldAndMeta, ZEnvelopeFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { type TFieldAndMeta, ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { parseFieldMetaFromPlaceholder, parseFieldTypeFromPlaceholder } from './helpers';
@@ -117,7 +117,7 @@ export const extractPlaceholdersFromPDF = async (pdf: Buffer): Promise<Placehold
const parsedFieldMeta = parseFieldMetaFromPlaceholder(rawFieldMeta, fieldType);
const fieldAndMeta: TFieldAndMeta = ZEnvelopeFieldAndMetaSchema.parse({
const fieldAndMeta: TFieldAndMeta = ZFieldAndMetaSchema.parse({
type: fieldType,
fieldMeta: parsedFieldMeta,
});
@@ -13,7 +13,6 @@ import { renderAuditLogs } from './render-audit-logs';
type GenerateAuditLogPdfOptions = GenerateCertificatePdfOptions & {
envelopeItems: string[];
additionalAuditLogs?: TDocumentAuditLog[];
};
export const generateAuditLogPdf = async (options: GenerateAuditLogPdfOptions) => {
@@ -25,7 +24,7 @@ export const generateAuditLogPdf = async (options: GenerateAuditLogPdfOptions) =
language,
pageWidth,
pageHeight,
additionalAuditLogs = [],
envelopeCompletedAuditLog,
} = options;
const documentLanguage = ZSupportedLanguageCodeSchema.parse(language);
@@ -41,7 +40,16 @@ export const generateAuditLogPdf = async (options: GenerateAuditLogPdfOptions) =
messages,
});
const auditLogs: TDocumentAuditLog[] = [...additionalAuditLogs, ...partialAuditLogs];
const auditLogs: TDocumentAuditLog[] = [...partialAuditLogs];
if (envelopeCompletedAuditLog) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
auditLogs.unshift({
...envelopeCompletedAuditLog,
id: '',
createdAt: new Date(),
} satisfies Omit<TDocumentAuditLog, 'type'> as TDocumentAuditLog);
}
const auditLogPages = await renderAuditLogs({
envelope,
@@ -7,6 +7,8 @@ import { FieldType } from '@prisma/client';
import { prop, sortBy } from 'remeda';
import { match } from 'ts-pattern';
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
import { ZSupportedLanguageCodeSchema } from '../../constants/i18n';
import type { TDocumentAuditLogBaseSchema } from '../../types/document-audit-logs';
import { extractDocumentAuthMethods } from '../../utils/document-auth';
@@ -37,6 +39,7 @@ export type GenerateCertificatePdfOptions = {
language?: string;
pageWidth: number;
pageHeight: number;
envelopeCompletedAuditLog?: CreateDocumentAuditLogDataResponse;
};
export const generateCertificatePdf = async (options: GenerateCertificatePdfOptions) => {
@@ -1,10 +1,4 @@
import type { PDFPage } from '@cantoo/pdf-lib';
import type { PDF } from '@libpdf/core';
import { PDF_SIZE_A4_72PPI } from '../../constants/pdf';
const MIN_CERT_PAGE_WIDTH = 300;
const MIN_CERT_PAGE_HEIGHT = 300;
/**
* Gets the effective page size for PDF operations.
@@ -22,20 +16,3 @@ export const getPageSize = (page: PDFPage) => {
return cropBox;
};
export const getLastPageDimensions = (pdfDoc: PDF): { width: number; height: number } => {
const lastPage = pdfDoc.getPage(pdfDoc.getPageCount() - 1);
if (!lastPage) {
return PDF_SIZE_A4_72PPI;
}
const width = Math.round(lastPage.width);
const height = Math.round(lastPage.height);
if (width < MIN_CERT_PAGE_WIDTH || height < MIN_CERT_PAGE_HEIGHT) {
return PDF_SIZE_A4_72PPI;
}
return { width, height };
};
@@ -1,24 +0,0 @@
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
export interface GetUserByResetTokenOptions {
token: string;
}
export const getUserByResetToken = async ({ token }: GetUserByResetTokenOptions) => {
const result = await prisma.passwordResetToken.findFirst({
where: {
token,
},
include: {
user: true,
},
});
if (!result || !result.user) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
return result.user;
};
@@ -1,27 +1,9 @@
import { prisma } from '@documenso/prisma';
const LEGACY_DELETED_ACCOUNT_EMAIL = 'deleted-account@documenso.com';
export const deletedServiceAccountEmail = () => {
try {
// eslint-disable-next-line turbo/no-undeclared-env-vars
if (process.env.NEXT_PRIVATE_DELETED_SERVICE_ACCOUNT_EMAIL) {
// eslint-disable-next-line turbo/no-undeclared-env-vars
return process.env.NEXT_PRIVATE_DELETED_SERVICE_ACCOUNT_EMAIL;
}
const { hostname } = new URL(process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000');
return `deleted-account@${hostname}`;
} catch (error) {
return LEGACY_DELETED_ACCOUNT_EMAIL;
}
};
export const deletedAccountServiceAccount = async () => {
const serviceAccount = await prisma.user.findFirst({
where: {
email: deletedServiceAccountEmail(),
email: 'deleted-account@documenso.com',
},
select: {
id: true,
@@ -47,20 +29,3 @@ export const deletedAccountServiceAccount = async () => {
return serviceAccount;
};
export const migrateDeletedAccountServiceAccount = async () => {
if (deletedServiceAccountEmail() !== LEGACY_DELETED_ACCOUNT_EMAIL) {
console.log(
`Migrating deleted account service account to new email: ${deletedServiceAccountEmail()}`,
);
await prisma.user.updateMany({
where: {
email: LEGACY_DELETED_ACCOUNT_EMAIL,
},
data: {
email: deletedServiceAccountEmail(),
},
});
}
};
@@ -1,34 +0,0 @@
import { prisma } from '@documenso/prisma';
const LEGACY_SERVICE_ACCOUNT_EMAIL = 'serviceaccount@documenso.com';
export const legacyServiceAccountEmail = () => {
try {
// eslint-disable-next-line turbo/no-undeclared-env-vars
if (process.env.NEXT_PRIVATE_LEGACY_SERVICE_ACCOUNT_EMAIL) {
// eslint-disable-next-line turbo/no-undeclared-env-vars
return process.env.NEXT_PRIVATE_LEGACY_SERVICE_ACCOUNT_EMAIL;
}
const { hostname } = new URL(process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000');
return `serviceaccount@${hostname}`;
} catch (error) {
return LEGACY_SERVICE_ACCOUNT_EMAIL;
}
};
export const migrateLegacyServiceAccount = async () => {
if (legacyServiceAccountEmail() !== LEGACY_SERVICE_ACCOUNT_EMAIL) {
console.log(`Migrating legacy service account to new email: ${legacyServiceAccountEmail()}`);
await prisma.user.updateMany({
where: {
email: LEGACY_SERVICE_ACCOUNT_EMAIL,
},
data: {
email: legacyServiceAccountEmail(),
},
});
}
};
File diff suppressed because it is too large Load Diff
+13 -268
View File
@@ -243,11 +243,6 @@ msgstr "{0} couldn't be uploaded:"
msgid "{0} direct signing templates"
msgstr "{0} direct signing templates"
#. placeholder {0}: t(FRIENDLY_FIELD_TYPE[field.type])
#: packages/ui/components/document/envelope-recipient-field-tooltip.tsx
msgid "{0} field"
msgstr "{0} field"
#. placeholder {0}: team.name
#. placeholder {1}: envelope.title
#: packages/lib/jobs/definitions/emails/send-signing-email.handler.ts
@@ -741,10 +736,6 @@ msgstr "2FA"
msgid "2FA Reset"
msgstr "2FA Reset"
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "2FA token"
msgstr "2FA token"
#: apps/remix/app/components/forms/token.tsx
msgid "3 months"
msgstr "3 months"
@@ -1421,10 +1412,6 @@ msgstr "All recipients have signed. The document is being processed and you will
msgid "All recipients will be notified"
msgstr "All recipients will be notified"
#: apps/remix/app/components/general/app-nav-mobile.tsx
msgid "All rights reserved."
msgstr "All rights reserved."
#: packages/email/template-components/template-document-cancel.tsx
msgid "All signatures have been voided."
msgstr "All signatures have been voided."
@@ -2039,10 +2026,6 @@ msgstr "Authentication required"
msgid "Authenticator app"
msgstr "Authenticator app"
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
msgid "Automatically sign fields"
msgstr "Automatically sign fields"
#: apps/remix/app/components/forms/avatar-image.tsx
msgid "Avatar"
msgstr "Avatar"
@@ -2294,7 +2277,6 @@ msgstr "Can't find someone?"
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
@@ -3061,7 +3043,6 @@ msgid "Created"
msgstr "Created"
#: apps/remix/app/components/tables/admin-organisations-table.tsx
#: apps/remix/app/components/tables/admin-user-teams-table.tsx
#: apps/remix/app/components/tables/user-organisations-table.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
#: packages/lib/server-only/pdf/render-audit-logs.ts
@@ -4188,7 +4169,6 @@ msgstr "Electronic Signature Disclosure"
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-add-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
@@ -4226,7 +4206,6 @@ msgstr "Electronic Signature Disclosure"
#: packages/ui/primitives/document-flow/add-fields.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/types.ts
#: packages/ui/primitives/template-flow/add-template-fields.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
@@ -5147,11 +5126,6 @@ msgstr "Groups"
msgid "Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist."
msgstr "Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist."
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
#: packages/ui/primitives/document-flow/field-item.tsx
msgid "Height:"
msgstr "Height:"
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
msgid "Help complete the document for other signers."
msgstr "Help complete the document for other signers."
@@ -5233,8 +5207,6 @@ msgstr "Hide"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/general/folder/folder-grid.tsx
#: apps/remix/app/components/general/generic-error-layout.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx
msgid "Home"
msgstr "Home"
@@ -5778,7 +5750,6 @@ msgstr "Loading Document..."
msgid "Loading suggestions..."
msgstr "Loading suggestions..."
#: apps/remix/app/components/embed/embed-client-loading.tsx
#: apps/remix/app/components/general/default-recipients-multiselect-combobox.tsx
#: apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
msgid "Loading..."
@@ -6133,8 +6104,6 @@ msgstr "Multiple access methods can be selected."
msgid "My Folder"
msgstr "My Folder"
#: apps/remix/app/components/tables/admin-document-logs-table.tsx
#: apps/remix/app/components/tables/document-logs-table.tsx
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: packages/lib/server-only/pdf/render-audit-logs.ts
@@ -6351,10 +6320,6 @@ msgstr "No signature field found"
msgid "No Stripe customer attached"
msgstr "No Stripe customer attached"
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "No subscription found"
msgstr "No subscription found"
#: packages/ui/components/recipient/recipient-autocomplete-input.tsx
msgid "No suggestions found"
msgstr "No suggestions found"
@@ -6367,10 +6332,6 @@ msgstr "No team groups found"
msgid "No teams yet"
msgstr "No teams yet"
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
msgid "No triggers available"
msgstr "No triggers available"
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
msgid "No valid direct templates found"
msgstr "No valid direct templates found"
@@ -6389,13 +6350,11 @@ 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/dialogs/envelope-distribute-dialog.tsx
#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx
#: apps/remix/app/components/forms/editor/editor-field-number-form.tsx
#: apps/remix/app/components/tables/admin-claims-table.tsx
#: apps/remix/app/components/tables/admin-organisations-table.tsx
#: packages/lib/constants/document.ts
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "None"
msgstr "None"
@@ -6542,7 +6501,6 @@ msgid "Or continue with"
msgstr "Or continue with"
#: apps/remix/app/components/tables/admin-organisations-table.tsx
#: apps/remix/app/components/tables/admin-user-teams-table.tsx
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
#: apps/remix/app/components/tables/user-organisations-table.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
@@ -6695,7 +6653,6 @@ msgstr "Override organisation settings"
#: apps/remix/app/components/tables/organisation-members-table.tsx
#: apps/remix/app/components/tables/user-organisations-table.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/dashboard.tsx
#: apps/remix/app/routes/_authenticated+/dashboard.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
@@ -6726,7 +6683,6 @@ msgstr "Paid"
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Passkey"
msgstr "Passkey"
@@ -7117,25 +7073,6 @@ msgstr "Please upload a document to continue"
msgid "Please upload a logo"
msgstr "Please upload a logo"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
#: packages/ui/primitives/document-flow/field-item.tsx
msgid "Pos X:"
msgstr "Pos X:"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
#: packages/ui/primitives/document-flow/field-item.tsx
msgid "Pos Y:"
msgstr "Pos Y:"
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
#: apps/remix/app/routes/embed+/v1+/multisign+/_index.tsx
#: apps/remix/app/routes/embed+/v1+/multisign+/_index.tsx
msgid "Powered by"
msgstr "Powered by"
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
msgid "Pre-formatted CSV template with example data."
msgstr "Pre-formatted CSV template with example data."
@@ -7812,7 +7749,6 @@ msgstr "Right"
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx
#: apps/remix/app/components/tables/admin-organisations-table.tsx
#: apps/remix/app/components/tables/admin-user-teams-table.tsx
#: apps/remix/app/components/tables/organisation-groups-table.tsx
#: apps/remix/app/components/tables/organisation-member-invites-table.tsx
#: apps/remix/app/components/tables/organisation-members-table.tsx
@@ -8305,7 +8241,6 @@ msgstr "Show templates in your public profile for your audience to sign and get
#: apps/remix/app/components/dialogs/sign-field-signature-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
@@ -8908,7 +8843,6 @@ msgstr "System Requirements"
msgid "System Theme"
msgstr "System Theme"
#: apps/remix/app/components/tables/admin-user-teams-table.tsx
#: apps/remix/app/components/tables/organisation-insights-table.tsx
#: apps/remix/app/components/tables/organisation-teams-table.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
@@ -8998,10 +8932,6 @@ msgstr "Team Members"
msgid "Team members have been added."
msgstr "Team members have been added."
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
msgid "Team Memberships"
msgstr "Team Memberships"
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
#: apps/remix/app/components/forms/team-update-form.tsx
#: apps/remix/app/components/tables/organisation-insights-table.tsx
@@ -9062,10 +8992,6 @@ msgstr "Teams help you organise your work and collaborate with others. Create yo
msgid "Teams that this organisation group is currently assigned to"
msgstr "Teams that this organisation group is currently assigned to"
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
msgid "Teams that this user is a member of and their roles."
msgstr "Teams that this user is a member of and their roles."
#: apps/remix/app/components/dialogs/organisation-member-invite-dialog.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/general/template/template-page-view-documents-table.tsx
@@ -9380,12 +9306,6 @@ msgstr "The following signers are missing signature fields:"
msgid "The following team has been deleted. You will no longer be able to access this team and its documents"
msgstr "The following team has been deleted. You will no longer be able to access this team and its documents"
#. placeholder {0}: form.watch('name')
#. placeholder {1}: form.watch('email')
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "The next recipient to sign this document will be <0>{0}</0> (<1>{1}</1>)."
msgstr "The next recipient to sign this document will be <0>{0}</0> (<1>{1}</1>)."
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
msgid "The OpenID discovery endpoint URL for your provider"
msgstr "The OpenID discovery endpoint URL for your provider"
@@ -9905,6 +9825,19 @@ msgstr "Title"
msgid "Title cannot be empty"
msgstr "Title cannot be empty"
#. placeholder {0}: actionVerb.toLowerCase()
#. placeholder {1}: actionTarget.toLowerCase()
#. placeholder {2}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To {0} this {1}, you need to be logged in as <0>{2}</0>"
msgstr "To {0} this {1}, you need to be logged in as <0>{2}</0>"
#. placeholder {0}: actionVerb.toLowerCase()
#. placeholder {1}: actionTarget.toLowerCase()
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To {0} this {1}, you need to be logged in."
msgstr "To {0} this {1}, you need to be logged in."
#: apps/remix/app/routes/_unauthenticated+/organisation.invite.$token.tsx
msgid "To accept this invitation you must create an account."
msgstr "To accept this invitation you must create an account."
@@ -9913,42 +9846,6 @@ msgstr "To accept this invitation you must create an account."
msgid "To add members to this team, you must first add them to the organisation."
msgstr "To add members to this team, you must first add them to the organisation."
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To approve this document, you need to be logged in as <0>{0}</0>"
msgstr "To approve this document, you need to be logged in as <0>{0}</0>"
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To approve this document, you need to be logged in."
msgstr "To approve this document, you need to be logged in."
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To approve this field, you need to be logged in as <0>{0}</0>"
msgstr "To approve this field, you need to be logged in as <0>{0}</0>"
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To approve this field, you need to be logged in."
msgstr "To approve this field, you need to be logged in."
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To assist with this document, you need to be logged in as <0>{0}</0>"
msgstr "To assist with this document, you need to be logged in as <0>{0}</0>"
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To assist with this document, you need to be logged in."
msgstr "To assist with this document, you need to be logged in."
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To assist with this field, you need to be logged in as <0>{0}</0>"
msgstr "To assist with this field, you need to be logged in as <0>{0}</0>"
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To assist with this field, you need to be logged in."
msgstr "To assist with this field, you need to be logged in."
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
msgid "To be able to add members to a team, you must first add them to the organisation. For more information, please see the <0>documentation</0>."
msgstr "To be able to add members to a team, you must first add them to the organisation. For more information, please see the <0>documentation</0>."
@@ -9996,24 +9893,6 @@ msgstr "To mark this document as viewed, you need to be logged in."
msgid "To proceed further, please set at least one value for the {0} field."
msgstr "To proceed further, please set at least one value for the {0} field."
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To sign this document, you need to be logged in as <0>{0}</0>"
msgstr "To sign this document, you need to be logged in as <0>{0}</0>"
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To sign this document, you need to be logged in."
msgstr "To sign this document, you need to be logged in."
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To sign this field, you need to be logged in as <0>{0}</0>"
msgstr "To sign this field, you need to be logged in as <0>{0}</0>"
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To sign this field, you need to be logged in."
msgstr "To sign this field, you need to be logged in."
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "To use our electronic signature service, you must have access to:"
msgstr "To use our electronic signature service, you must have access to:"
@@ -10022,26 +9901,6 @@ msgstr "To use our electronic signature service, you must have access to:"
msgid "To view this document you need to be signed into your account, please sign in to continue."
msgstr "To view this document you need to be signed into your account, please sign in to continue."
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To view this document, you need to be logged in as <0>{0}</0>"
msgstr "To view this document, you need to be logged in as <0>{0}</0>"
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To view this document, you need to be logged in."
msgstr "To view this document, you need to be logged in."
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To view this field, you need to be logged in as <0>{0}</0>"
msgstr "To view this field, you need to be logged in as <0>{0}</0>"
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To view this field, you need to be logged in."
msgstr "To view this field, you need to be logged in."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
msgid "Toggle the switch to hide your profile from the public."
msgstr "Toggle the switch to hide your profile from the public."
@@ -10272,7 +10131,6 @@ msgid "Unable to sign in"
msgstr "Unable to sign in"
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings._layout.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings._layout.tsx
@@ -11211,7 +11069,6 @@ msgid "We were unable to update your email preferences at this time, please try
msgstr "We were unable to update your email preferences at this time, please try again later"
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
msgid "We were unable to verify your details. Please try again or contact support"
msgstr "We were unable to verify your details. Please try again or contact support"
@@ -11381,11 +11238,6 @@ msgstr "Whitelabeling, unlimited members and more"
msgid "Who do you want to remind?"
msgstr "Who do you want to remind?"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
#: packages/ui/primitives/document-flow/field-item.tsx
msgid "Width:"
msgstr "Width:"
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Withdrawing Consent"
msgstr "Withdrawing Consent"
@@ -11929,80 +11781,10 @@ msgstr "You need to be logged in as <0>{email}</0> to view this page."
msgid "You need to be logged in to view this page."
msgstr "You need to be logged in to view this page."
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "You need to setup 2FA to approve this document."
msgstr "You need to setup 2FA to approve this document."
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "You need to setup 2FA to approve this field."
msgstr "You need to setup 2FA to approve this field."
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "You need to setup 2FA to assist with this document."
msgstr "You need to setup 2FA to assist with this document."
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "You need to setup 2FA to assist with this field."
msgstr "You need to setup 2FA to assist with this field."
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "You need to setup 2FA to mark this document as viewed."
msgstr "You need to setup 2FA to mark this document as viewed."
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "You need to setup 2FA to sign this document."
msgstr "You need to setup 2FA to sign this document."
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "You need to setup 2FA to sign this field."
msgstr "You need to setup 2FA to sign this field."
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "You need to setup 2FA to view this document."
msgstr "You need to setup 2FA to view this document."
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "You need to setup 2FA to view this field."
msgstr "You need to setup 2FA to view this field."
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "You need to setup a passkey to approve this document."
msgstr "You need to setup a passkey to approve this document."
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "You need to setup a passkey to approve this field."
msgstr "You need to setup a passkey to approve this field."
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "You need to setup a passkey to assist with this document."
msgstr "You need to setup a passkey to assist with this document."
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "You need to setup a passkey to assist with this field."
msgstr "You need to setup a passkey to assist with this field."
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "You need to setup a passkey to mark this document as viewed."
msgstr "You need to setup a passkey to mark this document as viewed."
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "You need to setup a passkey to sign this document."
msgstr "You need to setup a passkey to sign this document."
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "You need to setup a passkey to sign this field."
msgstr "You need to setup a passkey to sign this field."
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "You need to setup a passkey to view this document."
msgstr "You need to setup a passkey to view this document."
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "You need to setup a passkey to view this field."
msgstr "You need to setup a passkey to view this field."
#: packages/lib/utils/document-audit-logs.ts
msgid "You opened the document"
msgstr "You opened the document"
@@ -12140,43 +11922,6 @@ msgstr "Your brand website URL"
msgid "Your branding preferences have been updated"
msgstr "Your branding preferences have been updated"
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Your browser does not support passkeys, which is required to approve this document."
msgstr "Your browser does not support passkeys, which is required to approve this document."
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Your browser does not support passkeys, which is required to approve this field."
msgstr "Your browser does not support passkeys, which is required to approve this field."
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Your browser does not support passkeys, which is required to assist with this document."
msgstr "Your browser does not support passkeys, which is required to assist with this document."
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Your browser does not support passkeys, which is required to assist with this field."
msgstr "Your browser does not support passkeys, which is required to assist with this field."
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Your browser does not support passkeys, which is required to mark this document as viewed."
msgstr "Your browser does not support passkeys, which is required to mark this document as viewed."
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Your browser does not support passkeys, which is required to sign this document."
msgstr "Your browser does not support passkeys, which is required to sign this document."
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Your browser does not support passkeys, which is required to sign this field."
msgstr "Your browser does not support passkeys, which is required to sign this field."
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Your browser does not support passkeys, which is required to view this document."
msgstr "Your browser does not support passkeys, which is required to view this document."
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Your browser does not support passkeys, which is required to view this field."
msgstr "Your browser does not support passkeys, which is required to view this field."
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
msgid "Your bulk send has been initiated. You will receive an email notification upon completion."
msgstr "Your bulk send has been initiated. You will receive an email notification upon completion."
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+13 -268
View File
@@ -243,11 +243,6 @@ msgstr "{0} não pôde ser enviado:"
msgid "{0} direct signing templates"
msgstr "{0} modelos de assinatura direta"
#. placeholder {0}: t(FRIENDLY_FIELD_TYPE[field.type])
#: packages/ui/components/document/envelope-recipient-field-tooltip.tsx
msgid "{0} field"
msgstr ""
#. placeholder {0}: team.name
#. placeholder {1}: envelope.title
#: packages/lib/jobs/definitions/emails/send-signing-email.handler.ts
@@ -741,10 +736,6 @@ msgstr "2FA"
msgid "2FA Reset"
msgstr "Redefinição de 2FA"
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "2FA token"
msgstr ""
#: apps/remix/app/components/forms/token.tsx
msgid "3 months"
msgstr "3 meses"
@@ -1421,10 +1412,6 @@ msgstr "Todos os destinatários assinaram. O documento está sendo processado e
msgid "All recipients will be notified"
msgstr "Todos os destinatários serão notificados"
#: apps/remix/app/components/general/app-nav-mobile.tsx
msgid "All rights reserved."
msgstr ""
#: packages/email/template-components/template-document-cancel.tsx
msgid "All signatures have been voided."
msgstr "Todas as assinaturas foram anuladas."
@@ -2039,10 +2026,6 @@ msgstr "Autenticação necessária"
msgid "Authenticator app"
msgstr "Aplicativo autenticador"
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
msgid "Automatically sign fields"
msgstr ""
#: apps/remix/app/components/forms/avatar-image.tsx
msgid "Avatar"
msgstr "Avatar"
@@ -2294,7 +2277,6 @@ msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
@@ -3061,7 +3043,6 @@ msgid "Created"
msgstr "Criado"
#: apps/remix/app/components/tables/admin-organisations-table.tsx
#: apps/remix/app/components/tables/admin-user-teams-table.tsx
#: apps/remix/app/components/tables/user-organisations-table.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
#: packages/lib/server-only/pdf/render-audit-logs.ts
@@ -4188,7 +4169,6 @@ msgstr "Divulgação de Assinatura Eletrônica"
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-add-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
@@ -4226,7 +4206,6 @@ msgstr "Divulgação de Assinatura Eletrônica"
#: packages/ui/primitives/document-flow/add-fields.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/types.ts
#: packages/ui/primitives/template-flow/add-template-fields.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
@@ -5147,11 +5126,6 @@ msgstr "Grupos"
msgid "Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist."
msgstr "Ter um assistente como o último signatário significa que ele não poderá realizar nenhuma ação, pois não há signatários subsequentes para ajudar."
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
#: packages/ui/primitives/document-flow/field-item.tsx
msgid "Height:"
msgstr ""
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
msgid "Help complete the document for other signers."
msgstr "Ajude a completar o documento para outros signatários."
@@ -5233,8 +5207,6 @@ msgstr "Ocultar"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/general/folder/folder-grid.tsx
#: apps/remix/app/components/general/generic-error-layout.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx
msgid "Home"
msgstr "Início"
@@ -5778,7 +5750,6 @@ msgstr "Carregando Documento..."
msgid "Loading suggestions..."
msgstr "Carregando sugestões..."
#: apps/remix/app/components/embed/embed-client-loading.tsx
#: apps/remix/app/components/general/default-recipients-multiselect-combobox.tsx
#: apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
msgid "Loading..."
@@ -6133,8 +6104,6 @@ msgstr "Múltiplos métodos de acesso podem ser selecionados."
msgid "My Folder"
msgstr "Minha Pasta"
#: apps/remix/app/components/tables/admin-document-logs-table.tsx
#: apps/remix/app/components/tables/document-logs-table.tsx
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: packages/lib/server-only/pdf/render-audit-logs.ts
@@ -6351,10 +6320,6 @@ msgstr "Nenhum campo de assinatura encontrado"
msgid "No Stripe customer attached"
msgstr "Nenhum cliente Stripe anexado"
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "No subscription found"
msgstr ""
#: packages/ui/components/recipient/recipient-autocomplete-input.tsx
msgid "No suggestions found"
msgstr "Nenhuma sugestão encontrada"
@@ -6367,10 +6332,6 @@ msgstr "Nenhum grupo de equipe encontrado"
msgid "No teams yet"
msgstr "Nenhuma equipe ainda"
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
msgid "No triggers available"
msgstr ""
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
msgid "No valid direct templates found"
msgstr "Nenhum modelo direto válido encontrado"
@@ -6389,13 +6350,11 @@ msgstr "Nenhum valor encontrado."
msgid "No worries, it happens! Enter your email and we'll email you a special link to reset your password."
msgstr "Não se preocupe, acontece! Digite seu e-mail e enviaremos um link especial para redefinir sua senha."
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx
#: apps/remix/app/components/forms/editor/editor-field-number-form.tsx
#: apps/remix/app/components/tables/admin-claims-table.tsx
#: apps/remix/app/components/tables/admin-organisations-table.tsx
#: packages/lib/constants/document.ts
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "None"
msgstr "Nenhum"
@@ -6542,7 +6501,6 @@ msgid "Or continue with"
msgstr "Ou continue com"
#: apps/remix/app/components/tables/admin-organisations-table.tsx
#: apps/remix/app/components/tables/admin-user-teams-table.tsx
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
#: apps/remix/app/components/tables/user-organisations-table.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
@@ -6695,7 +6653,6 @@ msgstr "Substituir configurações da organização"
#: apps/remix/app/components/tables/organisation-members-table.tsx
#: apps/remix/app/components/tables/user-organisations-table.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/dashboard.tsx
#: apps/remix/app/routes/_authenticated+/dashboard.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
@@ -6726,7 +6683,6 @@ msgstr "Pago"
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Passkey"
msgstr "Passkey"
@@ -7117,25 +7073,6 @@ msgstr "Por favor, faça upload de um documento para continuar"
msgid "Please upload a logo"
msgstr "Por favor, faça upload de um logotipo"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
#: packages/ui/primitives/document-flow/field-item.tsx
msgid "Pos X:"
msgstr ""
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
#: packages/ui/primitives/document-flow/field-item.tsx
msgid "Pos Y:"
msgstr ""
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
#: apps/remix/app/routes/embed+/v1+/multisign+/_index.tsx
#: apps/remix/app/routes/embed+/v1+/multisign+/_index.tsx
msgid "Powered by"
msgstr ""
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
msgid "Pre-formatted CSV template with example data."
msgstr "Modelo CSV pré-formatado com dados de exemplo."
@@ -7812,7 +7749,6 @@ msgstr "Direita"
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx
#: apps/remix/app/components/tables/admin-organisations-table.tsx
#: apps/remix/app/components/tables/admin-user-teams-table.tsx
#: apps/remix/app/components/tables/organisation-groups-table.tsx
#: apps/remix/app/components/tables/organisation-member-invites-table.tsx
#: apps/remix/app/components/tables/organisation-members-table.tsx
@@ -8305,7 +8241,6 @@ msgstr "Mostre modelos em seu perfil público para seu público assinar e começ
#: apps/remix/app/components/dialogs/sign-field-signature-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
@@ -8908,7 +8843,6 @@ msgstr "Requisitos do Sistema"
msgid "System Theme"
msgstr "Tema do Sistema"
#: apps/remix/app/components/tables/admin-user-teams-table.tsx
#: apps/remix/app/components/tables/organisation-insights-table.tsx
#: apps/remix/app/components/tables/organisation-teams-table.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
@@ -8998,10 +8932,6 @@ msgstr "Membros da Equipe"
msgid "Team members have been added."
msgstr "Membros da equipe foram adicionados."
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
msgid "Team Memberships"
msgstr ""
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
#: apps/remix/app/components/forms/team-update-form.tsx
#: apps/remix/app/components/tables/organisation-insights-table.tsx
@@ -9062,10 +8992,6 @@ msgstr "Equipes ajudam você a organizar seu trabalho e colaborar com outros. Cr
msgid "Teams that this organisation group is currently assigned to"
msgstr "Equipes às quais este grupo da organização está atualmente atribuído"
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
msgid "Teams that this user is a member of and their roles."
msgstr ""
#: apps/remix/app/components/dialogs/organisation-member-invite-dialog.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/general/template/template-page-view-documents-table.tsx
@@ -9380,12 +9306,6 @@ msgstr "Os seguintes signatários estão sem campos de assinatura:"
msgid "The following team has been deleted. You will no longer be able to access this team and its documents"
msgstr "A seguinte equipe foi excluída. Você não poderá mais acessar esta equipe e seus documentos"
#. placeholder {0}: form.watch('name')
#. placeholder {1}: form.watch('email')
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "The next recipient to sign this document will be <0>{0}</0> (<1>{1}</1>)."
msgstr ""
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
msgid "The OpenID discovery endpoint URL for your provider"
msgstr "A URL do endpoint de descoberta OpenID para seu provedor"
@@ -9905,6 +9825,19 @@ msgstr "Título"
msgid "Title cannot be empty"
msgstr "O título não pode estar vazio"
#. placeholder {0}: actionVerb.toLowerCase()
#. placeholder {1}: actionTarget.toLowerCase()
#. placeholder {2}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To {0} this {1}, you need to be logged in as <0>{2}</0>"
msgstr "Para {0} este {1}, você precisa estar logado como <0>{2}</0>"
#. placeholder {0}: actionVerb.toLowerCase()
#. placeholder {1}: actionTarget.toLowerCase()
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To {0} this {1}, you need to be logged in."
msgstr "Para {0} este {1}, você precisa estar logado."
#: apps/remix/app/routes/_unauthenticated+/organisation.invite.$token.tsx
msgid "To accept this invitation you must create an account."
msgstr "Para aceitar este convite, você deve criar uma conta."
@@ -9913,42 +9846,6 @@ msgstr "Para aceitar este convite, você deve criar uma conta."
msgid "To add members to this team, you must first add them to the organisation."
msgstr ""
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To approve this document, you need to be logged in as <0>{0}</0>"
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To approve this document, you need to be logged in."
msgstr ""
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To approve this field, you need to be logged in as <0>{0}</0>"
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To approve this field, you need to be logged in."
msgstr ""
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To assist with this document, you need to be logged in as <0>{0}</0>"
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To assist with this document, you need to be logged in."
msgstr ""
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To assist with this field, you need to be logged in as <0>{0}</0>"
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To assist with this field, you need to be logged in."
msgstr ""
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
msgid "To be able to add members to a team, you must first add them to the organisation. For more information, please see the <0>documentation</0>."
msgstr "Para poder adicionar membros a uma equipe, você deve primeiro adicioná-los à organização. Para mais informações, consulte a <0>documentação</0>."
@@ -9996,24 +9893,6 @@ msgstr "Para marcar este documento como visualizado, você precisa estar logado.
msgid "To proceed further, please set at least one value for the {0} field."
msgstr "Para prosseguir, defina pelo menos um valor para o campo {0}."
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To sign this document, you need to be logged in as <0>{0}</0>"
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To sign this document, you need to be logged in."
msgstr ""
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To sign this field, you need to be logged in as <0>{0}</0>"
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To sign this field, you need to be logged in."
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "To use our electronic signature service, you must have access to:"
msgstr "Para usar nosso serviço de assinatura eletrônica, você deve ter acesso a:"
@@ -10022,26 +9901,6 @@ msgstr "Para usar nosso serviço de assinatura eletrônica, você deve ter acess
msgid "To view this document you need to be signed into your account, please sign in to continue."
msgstr "Para visualizar este documento, você precisa estar logado em sua conta, faça login para continuar."
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To view this document, you need to be logged in as <0>{0}</0>"
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To view this document, you need to be logged in."
msgstr ""
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To view this field, you need to be logged in as <0>{0}</0>"
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To view this field, you need to be logged in."
msgstr ""
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
msgid "Toggle the switch to hide your profile from the public."
msgstr "Alterne o botão para ocultar seu perfil do público."
@@ -10272,7 +10131,6 @@ msgid "Unable to sign in"
msgstr "Não foi possível entrar"
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings._layout.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings._layout.tsx
@@ -11211,7 +11069,6 @@ msgid "We were unable to update your email preferences at this time, please try
msgstr "Não conseguimos atualizar suas preferências de e-mail neste momento, por favor, tente novamente mais tarde"
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
msgid "We were unable to verify your details. Please try again or contact support"
msgstr "Não conseguimos verificar seus detalhes. Por favor, tente novamente ou entre em contato com o suporte"
@@ -11381,11 +11238,6 @@ msgstr "Whitelabeling, membros ilimitados e mais"
msgid "Who do you want to remind?"
msgstr "Quem você deseja lembrar?"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
#: packages/ui/primitives/document-flow/field-item.tsx
msgid "Width:"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Withdrawing Consent"
msgstr "Retirada de Consentimento"
@@ -11929,80 +11781,10 @@ msgstr "Você precisa estar logado como <0>{email}</0> para visualizar esta pág
msgid "You need to be logged in to view this page."
msgstr "Você precisa estar logado para visualizar esta página."
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "You need to setup 2FA to approve this document."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "You need to setup 2FA to approve this field."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "You need to setup 2FA to assist with this document."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "You need to setup 2FA to assist with this field."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "You need to setup 2FA to mark this document as viewed."
msgstr "Você precisa configurar a 2FA para marcar este documento como visualizado."
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "You need to setup 2FA to sign this document."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "You need to setup 2FA to sign this field."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "You need to setup 2FA to view this document."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "You need to setup 2FA to view this field."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "You need to setup a passkey to approve this document."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "You need to setup a passkey to approve this field."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "You need to setup a passkey to assist with this document."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "You need to setup a passkey to assist with this field."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "You need to setup a passkey to mark this document as viewed."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "You need to setup a passkey to sign this document."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "You need to setup a passkey to sign this field."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "You need to setup a passkey to view this document."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "You need to setup a passkey to view this field."
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "You opened the document"
msgstr ""
@@ -12140,43 +11922,6 @@ msgstr "URL do site da sua marca"
msgid "Your branding preferences have been updated"
msgstr "Suas preferências de marca foram atualizadas"
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Your browser does not support passkeys, which is required to approve this document."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Your browser does not support passkeys, which is required to approve this field."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Your browser does not support passkeys, which is required to assist with this document."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Your browser does not support passkeys, which is required to assist with this field."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Your browser does not support passkeys, which is required to mark this document as viewed."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Your browser does not support passkeys, which is required to sign this document."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Your browser does not support passkeys, which is required to sign this field."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Your browser does not support passkeys, which is required to view this document."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Your browser does not support passkeys, which is required to view this field."
msgstr ""
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
msgid "Your bulk send has been initiated. You will receive an email notification upon completion."
msgstr "Seu envio em massa foi iniciado. Você receberá uma notificação por e-mail após a conclusão."
File diff suppressed because it is too large Load Diff
@@ -37,7 +37,7 @@ const ZCreateFieldBaseSchema = ZEnvelopeFieldAndMetaSchema.and(
/**
* Position a field using explicit percentage-based coordinates.
*/
export const ZCoordinatePositionSchema = z.object({
const ZCoordinatePositionSchema = z.object({
page: ZFieldPageNumberSchema,
positionX: ZClampedFieldPositionXSchema,
positionY: ZClampedFieldPositionYSchema,
@@ -52,7 +52,7 @@ export const ZCoordinatePositionSchema = z.object({
* placed at the bounding box of that match. Width and height can optionally be
* overridden; when omitted the dimensions of the placeholder text are used.
*/
export const ZPlaceholderPositionSchema = z.object({
const ZPlaceholderPositionSchema = z.object({
placeholder: z
.string()
.describe(
@@ -72,10 +72,9 @@ export const ZPlaceholderPositionSchema = z.object({
),
});
const ZCreateFieldSchema = z.union([
ZCreateFieldBaseSchema.and(ZCoordinatePositionSchema),
ZCreateFieldBaseSchema.and(ZPlaceholderPositionSchema),
]);
const ZCreateFieldSchema = ZCreateFieldBaseSchema.and(
z.union([ZCoordinatePositionSchema, ZPlaceholderPositionSchema]),
);
export const ZCreateEnvelopeFieldsRequestSchema = z.object({
envelopeId: z.string(),
@@ -1,10 +1,16 @@
import { z } from 'zod';
import { ZFieldSchema } from '@documenso/lib/types/field';
import {
ZClampedFieldHeightSchema,
ZClampedFieldPositionXSchema,
ZClampedFieldPositionYSchema,
ZClampedFieldWidthSchema,
ZFieldPageNumberSchema,
ZFieldSchema,
} from '@documenso/lib/types/field';
import { ZEnvelopeFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import type { TrpcRouteMeta } from '../../trpc';
import { ZCoordinatePositionSchema } from './create-envelope-fields.types';
export const updateEnvelopeFieldsMeta: TrpcRouteMeta = {
openapi: {
@@ -16,7 +22,7 @@ export const updateEnvelopeFieldsMeta: TrpcRouteMeta = {
},
};
const ZUpdateFieldBaseSchema = ZEnvelopeFieldAndMetaSchema.and(
const ZUpdateFieldSchema = ZEnvelopeFieldAndMetaSchema.and(
z.object({
id: z.number().describe('The ID of the field to update.'),
envelopeItemId: z
@@ -25,18 +31,14 @@ const ZUpdateFieldBaseSchema = ZEnvelopeFieldAndMetaSchema.and(
.describe(
'The ID of the envelope item to put the field on. If not provided, field will be placed on the first item.',
),
page: ZFieldPageNumberSchema.optional(),
positionX: ZClampedFieldPositionXSchema.optional(),
positionY: ZClampedFieldPositionYSchema.optional(),
width: ZClampedFieldWidthSchema.optional(),
height: ZClampedFieldHeightSchema.optional(),
}),
);
// !: Later on we should support placeholders for updates, but since we didn't do the
// !: neccesary plumbing for updates in the initial release, we can hold off for now.
// const ZUpdateFieldSchema = z.union([
// ZUpdateFieldBaseSchema.and(ZCoordinatePositionSchema),
// ZUpdateFieldBaseSchema.and(ZPlaceholderPositionSchema),
// ]);
const ZUpdateFieldSchema = ZUpdateFieldBaseSchema.and(ZCoordinatePositionSchema.partial());
export const ZUpdateEnvelopeFieldsRequestSchema = z.object({
envelopeId: z.string(),
data: ZUpdateFieldSchema.array(),
@@ -37,7 +37,7 @@ export const DocumentEmailCheckboxes = ({
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
htmlFor={DocumentEmailEvents.RecipientSigned}
>
<Trans>Email the owner when a recipient signs</Trans>
<Trans>Send recipient signed email</Trans>
<Tooltip>
<TooltipTrigger>
@@ -75,7 +75,7 @@ export const DocumentEmailCheckboxes = ({
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
htmlFor={DocumentEmailEvents.RecipientSigningRequest}
>
<Trans>Email recipients with a signing request</Trans>
<Trans>Send recipient signing request email</Trans>
<Tooltip>
<TooltipTrigger>
@@ -113,7 +113,7 @@ export const DocumentEmailCheckboxes = ({
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
htmlFor={DocumentEmailEvents.RecipientRemoved}
>
<Trans>Email recipients when they're removed from a pending document</Trans>
<Trans>Send recipient removed email</Trans>
<Tooltip>
<TooltipTrigger>
@@ -151,7 +151,7 @@ export const DocumentEmailCheckboxes = ({
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
htmlFor={DocumentEmailEvents.DocumentPending}
>
<Trans>Email the signer if the document is still pending</Trans>
<Trans>Send document pending email</Trans>
<Tooltip>
<TooltipTrigger>
@@ -190,7 +190,7 @@ export const DocumentEmailCheckboxes = ({
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
htmlFor={DocumentEmailEvents.DocumentCompleted}
>
<Trans>Email recipients when the document is completed</Trans>
<Trans>Send document completed email</Trans>
<Tooltip>
<TooltipTrigger>
@@ -228,7 +228,7 @@ export const DocumentEmailCheckboxes = ({
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
htmlFor={DocumentEmailEvents.DocumentDeleted}
>
<Trans>Email recipients when a pending document is deleted</Trans>
<Trans>Send document deleted email</Trans>
<Tooltip>
<TooltipTrigger>
@@ -266,7 +266,7 @@ export const DocumentEmailCheckboxes = ({
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
htmlFor={DocumentEmailEvents.OwnerDocumentCompleted}
>
<Trans>Email the owner when the document is completed</Trans>
<Trans>Send document completed email to the owner</Trans>
<Tooltip>
<TooltipTrigger>
@@ -186,9 +186,7 @@ export function EnvelopeRecipientFieldTooltip({
)}
<p className="text-center font-semibold">
<span>
<Trans>{t(FRIENDLY_FIELD_TYPE[field.type])} field</Trans>
</span>
<span>{t(FRIENDLY_FIELD_TYPE[field.type])} field</span>
</p>
<p className="text-muted-foreground mt-1 text-center text-xs">
@@ -1,6 +1,5 @@
import React, { Suspense, lazy } from 'react';
import { Trans } from '@lingui/react/macro';
import { type PDFDocumentProxy } from 'pdfjs-dist';
import type { PdfViewerRendererMode } from './pdf-viewer-konva';
@@ -18,13 +17,7 @@ const EnvelopePdfViewer = lazy(async () => import('./pdf-viewer-konva'));
export const PDFViewerKonvaLazy = (props: PDFViewerProps) => {
return (
<Suspense
fallback={
<div>
<Trans>Loading...</Trans>
</div>
}
>
<Suspense fallback={<div>Loading...</div>}>
<EnvelopePdfViewer {...props} />
</Suspense>
);
@@ -191,10 +191,10 @@ export const AddSubjectFormPartial = ({
>
<TabsList className="w-full">
<TabsTrigger className="w-full" value={DocumentDistributionMethod.EMAIL}>
<Trans>Email</Trans>
Email
</TabsTrigger>
<TabsTrigger className="w-full" value={DocumentDistributionMethod.NONE}>
<Trans>None</Trans>
None
</TabsTrigger>
</TabsList>
</Tabs>
@@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { FieldType } from '@prisma/client';
import { CopyPlus, Settings2, SquareStack, Trash } from 'lucide-react';
import { createPortal } from 'react-dom';
@@ -325,31 +324,19 @@ export const FieldItem = ({
<div className="absolute -top-20 left-1/2 z-50 -translate-x-1/2 rounded-md border border-border bg-background/95 px-2 py-1 shadow-sm backdrop-blur-sm">
<div className="flex flex-col gap-0.5 text-[9px]">
<span>
<span className="text-muted-foreground">
<Trans>Pos X:</Trans>
</span>
&nbsp;
<span className="text-muted-foreground">Pos X:&nbsp;</span>
<span className="font-mono text-foreground">{field.pageX.toFixed(2)}</span>
</span>
<span>
<span className="text-muted-foreground">
<Trans>Pos Y:</Trans>
</span>
&nbsp;
<span className="text-muted-foreground">Pos Y:&nbsp;</span>
<span className="font-mono text-foreground">{field.pageY.toFixed(2)}</span>
</span>
<span>
<span className="text-muted-foreground">
<Trans>Width:</Trans>
</span>
&nbsp;
<span className="text-muted-foreground">Width:&nbsp;</span>
<span className="font-mono text-foreground">{field.pageWidth.toFixed(2)}</span>
</span>
<span>
<span className="text-muted-foreground">
<Trans>Height:</Trans>
</span>
&nbsp;
<span className="text-muted-foreground">Height:&nbsp;</span>
<span className="font-mono text-foreground">{field.pageHeight.toFixed(2)}</span>
</span>
</div>
+1 -13
View File
@@ -1,19 +1,7 @@
import { ClientOnly } from '../../components/client-only';
import { Trans } from '@lingui/react/macro';
import { PDFViewer, type PDFViewerProps } from './base.client';
export const PDFViewerLazy = (props: PDFViewerProps) => {
return (
<ClientOnly
fallback={
<div>
<Trans>Loading...</Trans>
</div>
}
>
{() => <PDFViewer {...props} />}
</ClientOnly>
);
return <ClientOnly fallback={<div>Loading...</div>}>{() => <PDFViewer {...props} />}</ClientOnly>;
};