Compare commits

...

19 Commits

Author SHA1 Message Date
David Nguyen 18d092f415 fix: wip 2026-02-26 14:33:01 +11:00
David Nguyen 6425b242f0 Merge branch 'main' into feat/add-pdf-image-renderer 2026-02-25 19:26:47 +11:00
David Nguyen 8e8f57661c fix: refactors 2026-02-25 19:26:09 +11:00
Lucas Smith 6f5014a561 feat: support optional read replicas (#2540) 2026-02-25 19:07:02 +11:00
David Nguyen 653d340668 fix: virtualisation issues 2026-02-25 17:30:20 +11:00
Lucas Smith c112392da9 feat: add admin email domain management and sync job (#2538) 2026-02-25 15:14:18 +11:00
github-actions[bot] bc72d9cb17 chore: extract translations (#2505) 2026-02-24 22:07:03 +11:00
David Nguyen 9a2f3747db fix: reorder migrations 2026-02-24 21:09:38 +11:00
David Nguyen 84deea11e4 Merge branch 'main' into feat/add-pdf-image-renderer 2026-02-24 18:51:04 +11:00
David Nguyen 5fa4c42098 fix: refactor how non-s3 pdfs are loaded 2026-02-24 18:47:50 +11:00
Karlo 3ad3216c4c fix: update button width to fit content in public profile page (#2506)
Co-authored-by: Catalin Pit <catalinpit@gmail.com>
2026-02-23 12:46:58 +02:00
Lucas Smith 36eef79b1a fix: omit fieldId from embed create endpoints (#2523) 2026-02-21 21:14:51 +11:00
Lucas Smith 6fb88fede5 chore: upgrade libpdf (#2522) 2026-02-21 20:54:33 +11:00
David Nguyen ab3e8a4074 fix: pdf viewer scroll elements 2026-02-06 14:59:52 +11:00
David Nguyen cb6d6e46d0 fix: replace etag with hard cache 2026-02-06 13:35:41 +11:00
David Nguyen c20affa286 Merge branch 'main' into feat/add-pdf-image-renderer 2026-02-04 12:50:40 +11:00
David Nguyen a69fe940b5 fix: refactor 2026-01-27 15:42:35 +11:00
David Nguyen 8186d2817f fix: add client side pdf render 2026-01-27 15:12:59 +11:00
David Nguyen 4fb3c2cb0f feat: add pdf image renderer 2026-01-27 14:39:16 +11:00
136 changed files with 5816 additions and 1328 deletions
@@ -0,0 +1,263 @@
---
date: 2026-02-24
title: Custom Email Domain Sync And Recovery
---
## Problem Statement
Custom email domains configured via AWS SES can get stuck in a `PENDING` state or fail validation silently. Currently, there is **no automated verification** -- users must manually click "Sync" in the UI to check domain status. If a domain fails to validate, the only option is to delete it and recreate it, which generates new DKIM keys and requires the user to update their DNS records.
### Current Pain Points
1. **No background sync** -- Domain verification status is never checked automatically; users must manually click "Sync"
2. **Stuck domains** -- Domains can remain in `PENDING` state indefinitely with no alerting or auto-recovery
3. **Failed recovery requires DNS changes** -- Deleting and recreating a domain generates new keys, forcing the user to update DNS records
4. **No visibility into failure duration** -- There's no tracking of how long a domain has been pending
## Proposed Solution
### 1. Hourly Background Sync Job
Create a new cron job (`internal.sync-email-domains`) that runs every hour to automatically verify all `PENDING` email domains.
**Job Definition:** `packages/lib/jobs/definitions/internal/sync-email-domains.ts`
**Job Handler:** `packages/lib/jobs/definitions/internal/sync-email-domains.handler.ts`
**Pattern:** Follow the existing `cleanup-rate-limits` cron job pattern:
- `cron: '0 * * * *'` (every hour, on the hour)
- Empty `z.object({})` schema (no payload needed)
- Register in `packages/lib/jobs/client.ts`
**Handler Logic:**
1. Query all `EmailDomain` records with `status: 'PENDING'`
2. For each domain, call `verifyEmailDomain(emailDomainId)` which:
- Calls AWS SES `GetEmailIdentityCommand` to check current verification status
- Updates DB status to `ACTIVE` if verified, keeps `PENDING` otherwise
3. Log results via `io.logger` (how many checked, how many transitioned to ACTIVE)
4. Process domains in batches to avoid overwhelming SES API rate limits
5. Add error handling per-domain so one failure doesn't stop the entire sweep
### 2. Schema Changes -- Track Pending Duration
Add a `lastVerifiedAt` column to the `EmailDomain` model to track when verification was last attempted, enabling "stale domain" detection.
**File:** `packages/prisma/schema.prisma`
```prisma
model EmailDomain {
// ... existing fields ...
lastVerifiedAt DateTime? // Last time verification was checked against SES
}
```
**Migration:** Create a new Prisma migration for this column addition.
**Updates needed:**
- `verify-email-domain.ts` -- Update `lastVerifiedAt` when verification is checked
- The sync job handler -- Use `lastVerifiedAt` to avoid re-checking domains that were just verified
### 3. Domain Re-registration (Recovery) -- Delete & Recreate in SES Without Changing Keys
Add a new "Re-register" action that deletes the SES identity and recreates it using the **same** DKIM key pair stored in the database, so the user's DNS records remain valid.
#### 3a. New Service Function
**File:** `packages/ee/server-only/lib/reregister-email-domain.ts`
```typescript
export const reregisterEmailDomain = async (options: { emailDomainId: string }) => {
// 1. Fetch the EmailDomain record (including encrypted privateKey)
// 2. Decrypt the private key using DOCUMENSO_ENCRYPTION_KEY
// 3. Call DeleteEmailIdentityCommand on SES (ignore NotFoundException)
// 4. Call CreateEmailIdentityCommand with BYODKIM using the SAME selector + private key
// 5. Update EmailDomain status back to PENDING, update lastVerifiedAt
// 6. Return the updated domain
};
```
Key points:
- Uses the existing encrypted `privateKey` from the DB -- no new key generation
- Uses the existing `selector` -- DNS records stay the same
- Deletes first, then recreates -- handles cases where SES state is corrupted
- Resets status to `PENDING` since verification will need to re-occur
- Uses `verifyDomainWithDKIM()` from `create-email-domain.ts` (may need to extract/export this helper)
#### 3b. Admin TRPC Routes (Find, Get, Re-register)
All email domain admin routes use `adminProcedure` -- requires system-level `Role.ADMIN`.
**Find (list) route:**
**File:** `packages/trpc/server/admin-router/find-email-domains.ts`
**Types:** `packages/trpc/server/admin-router/find-email-domains.types.ts`
- Query route: `admin.emailDomain.find`
- Input: `{ query?: string, page?: number, perPage?: number, status?: EmailDomainStatus }`
- Extends `ZFindSearchParamsSchema` with optional `status` filter
- Returns standard `ZFindResultResponse` with email domain data including: id, domain, status, selector, createdAt, lastVerifiedAt, organisation name, email count
- Prisma query filters by domain name (LIKE search on `query`), optional status, joins organisation for name, counts emails
**Get (detail) route:**
**File:** `packages/trpc/server/admin-router/get-email-domain.ts`
**Types:** `packages/trpc/server/admin-router/get-email-domain.types.ts`
- Query route: `admin.emailDomain.get`
- Input: `{ emailDomainId: string }`
- Returns full email domain detail: all fields (except privateKey), organisation info, list of associated emails, DNS records (generated from publicKey + selector)
- Omits `privateKey` from response
**Re-register (mutation) route:**
**File:** `packages/trpc/server/admin-router/reregister-email-domain.ts`
**Types:** `packages/trpc/server/admin-router/reregister-email-domain.types.ts`
- Mutation route: `admin.emailDomain.reregister`
- Input: `{ emailDomainId: string }`
- Calls `reregisterEmailDomain()`
- Rationale: Re-registration is a recovery/operational action that deletes and recreates an SES identity. This is a privileged operation that should only be performed by platform operators, not self-service by org admins.
#### 3c. Register in Admin Router
**File:** `packages/trpc/server/admin-router/router.ts`
Add a new `emailDomain` namespace to the admin router:
```typescript
emailDomain: {
find: findEmailDomainsRoute,
get: getEmailDomainRoute,
reregister: reregisterEmailDomainRoute,
},
```
#### 3d. Admin Panel UI -- Email Domains Section
**List page:** `apps/remix/app/routes/_authenticated+/admin+/email-domains._index.tsx`
- New admin panel page at `/admin/email-domains`
- Follow the existing admin documents list pattern (client-side TRPC data fetching)
- Search input (debounced) filtering by domain name
- Status filter dropdown (All / Pending / Active)
- DataTable with columns: Domain, Organisation, Status (badge), Email Count, Created, Last Verified, Actions
- Actions dropdown per row: View details, Re-register
- Pagination via `DataTablePagination`
**Detail page:** `apps/remix/app/routes/_authenticated+/admin+/email-domains.$id.tsx`
- Shows full domain details: domain, selector, status, organisation, created date, last verified date
- Shows DNS records (DKIM + SPF) with copy buttons (reuse `organisation-email-domain-records-dialog` pattern)
- Table of associated organisation emails
- "Re-register" button with confirmation dialog explaining the action (SES identity will be deleted and recreated with the same keys)
- "Verify Now" button to manually trigger a verification check
- Shows how long the domain has been pending (using `lastVerifiedAt` or `createdAt`)
**Navigation:** Add menu item to admin sidebar in `_layout.tsx`:
```tsx
<Button
variant="ghost"
className={cn(
'justify-start md:w-full',
pathname?.startsWith('/admin/email-domains') && 'bg-secondary',
)}
asChild
>
<Link to="/admin/email-domains">
<MailIcon className="mr-2 h-5 w-5" />
<Trans>Email Domains</Trans>
</Link>
</Button>
```
**Table component:** `apps/remix/app/components/tables/admin-email-domains-table.tsx` (optional -- can be inline in the route file like the documents page)
#### 3e. Automatic Re-registration in Sync Job (Optional Enhancement)
In the hourly sync job, after checking verification status, if a domain has been `PENDING` for more than 48 hours:
- Automatically call `reregisterEmailDomain()` to attempt recovery
- Log the auto-recovery attempt
- This provides a self-healing mechanism without user intervention
## Implementation Plan
### Phase 1: Background Sync Job (Core)
1. Create `sync-email-domains.ts` job definition with hourly cron
2. Create `sync-email-domains.handler.ts` with batch verification logic
3. Register job in `packages/lib/jobs/client.ts`
4. Add error handling and logging
### Phase 2: Schema Enhancement
5. Add `lastVerifiedAt` column to `EmailDomain` model
6. Create Prisma migration
7. Update `verifyEmailDomain()` to set `lastVerifiedAt` on each check
8. Update sync job to use `lastVerifiedAt` for intelligent scheduling
### Phase 3: Admin Email Domains Panel
9. Create `find-email-domains` admin TRPC route + types (list/search with pagination and status filter)
10. Create `get-email-domain` admin TRPC route + types (detail view with org info, emails, DNS records)
11. Register find + get routes in admin router under `emailDomain` namespace
12. Create admin list page (`admin+/email-domains._index.tsx`) with search, status filter, DataTable
13. Create admin detail page (`admin+/email-domains.$id.tsx`) with domain info, emails table, DNS records
14. Add "Email Domains" menu item to admin sidebar (`_layout.tsx`)
### Phase 4: Re-registration Feature
15. Extract `verifyDomainWithDKIM()` as a shared helper (if not already exported)
16. Create `reregisterEmailDomain()` service function
17. Create `reregister-email-domain` admin TRPC mutation route + types
18. Register reregister route in admin router under `emailDomain.reregister`
19. Add "Re-register" button + confirmation dialog on admin detail page
### Phase 5: Auto-Recovery (Optional)
20. Add 48-hour stale detection logic to sync job
21. Auto-trigger re-registration for stale domains
22. Add logging/notifications for auto-recovery events
## Files to Create/Modify
### New Files
- `packages/lib/jobs/definitions/internal/sync-email-domains.ts`
- `packages/lib/jobs/definitions/internal/sync-email-domains.handler.ts`
- `packages/ee/server-only/lib/reregister-email-domain.ts`
- `packages/trpc/server/admin-router/find-email-domains.ts`
- `packages/trpc/server/admin-router/find-email-domains.types.ts`
- `packages/trpc/server/admin-router/get-email-domain.ts`
- `packages/trpc/server/admin-router/get-email-domain.types.ts`
- `packages/trpc/server/admin-router/reregister-email-domain.ts`
- `packages/trpc/server/admin-router/reregister-email-domain.types.ts`
- `apps/remix/app/routes/_authenticated+/admin+/email-domains._index.tsx`
- `apps/remix/app/routes/_authenticated+/admin+/email-domains.$id.tsx`
### Modified Files
- `packages/prisma/schema.prisma` -- Add `lastVerifiedAt` field
- `packages/lib/jobs/client.ts` -- Register new sync job
- `packages/ee/server-only/lib/verify-email-domain.ts` -- Update `lastVerifiedAt`
- `packages/ee/server-only/lib/create-email-domain.ts` -- Export `verifyDomainWithDKIM` helper
- `packages/trpc/server/admin-router/router.ts` -- Add `emailDomain.{find, get, reregister}` routes
- `apps/remix/app/routes/_authenticated+/admin+/_layout.tsx` -- Add "Email Domains" nav item to sidebar
- New Prisma migration file
## Technical Considerations
1. **SES API Rate Limits** -- AWS SES has rate limits on `GetEmailIdentityCommand`. The sync job should process domains in batches with small delays between calls (e.g., 5-10 per batch with 1s delay).
2. **Concurrency** -- The local job provider has deterministic deduplication via SHA-256 IDs, so multiple app instances won't run the same cron tick twice.
3. **Error Isolation** -- Each domain verification in the sync job should be wrapped in try/catch so one failing domain doesn't prevent others from being checked.
4. **Re-registration Safety** -- The re-register function should be idempotent. Deleting a non-existent SES identity should be handled gracefully (already done in `deleteEmailDomain`).
5. **Private Key Security** -- The private key is encrypted at rest and should only be decrypted transiently during re-registration. It should never be logged or exposed in API responses.
6. **Feature Gating** -- The sync job should only process domains belonging to organisations with active `emailDomains` claim flags. This prevents processing domains for orgs that have downgraded.
7. **Observability** -- Add structured logging to the sync job so operations teams can monitor domain verification health across all tenants.
@@ -115,7 +115,7 @@ export function AssistantConfirmationDialog({
<div className="mt-4 flex flex-col gap-4">
{!isEditingNextSigner && (
<div>
<p className="text-muted-foreground text-sm">
<p className="text-sm text-muted-foreground">
<Trans>
The next recipient to sign this document will be{' '}
<span className="font-semibold">{form.watch('name')}</span> (
@@ -1,3 +1,5 @@
import { useRef } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
@@ -5,6 +7,7 @@ import { useNavigate } from 'react-router';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc as trpcReact } from '@documenso/trpc/react';
import { PDFViewer } from '@documenso/ui/components/pdf-viewer/pdf-viewer';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
@@ -13,7 +16,6 @@ import {
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCurrentTeam } from '~/providers/team';
@@ -38,6 +40,8 @@ export const DocumentDuplicateDialog = ({
const team = useCurrentTeam();
const scrollContainerRef = useRef<HTMLDivElement>(null);
const { data: envelopeItemsPayload, isLoading: isLoadingEnvelopeItems } =
trpcReact.envelope.item.getManyByToken.useQuery(
{
@@ -95,12 +99,13 @@ export const DocumentDuplicateDialog = ({
</h1>
</div>
) : (
<div className="p-2 [&>div]:h-[50vh] [&>div]:overflow-y-scroll">
<PDFViewerLazy
<div ref={scrollContainerRef} className="h-[50vh] overflow-y-scroll p-2">
<PDFViewer
key={envelopeItems[0].id}
envelopeItem={envelopeItems[0]}
token={undefined}
version="original"
version="initial"
scrollParentRef={scrollContainerRef}
/>
</div>
)}
@@ -8,6 +8,8 @@ import { useDropzone } from 'react-dropzone';
import { useFormContext } from 'react-hook-form';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { PDF_IMAGE_RENDER_SCALE } from '@documenso/lib/constants/pdf-viewer';
import { pdfToImagesClientSide } from '@documenso/lib/server-only/ai/pdf-to-images.client';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import {
@@ -52,12 +54,17 @@ export const ConfigureDocumentUpload = ({ isSubmitting = false }: ConfigureDocum
const arrayBuffer = await file.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
const pdfImages = await pdfToImagesClientSide(uint8Array, {
scale: PDF_IMAGE_RENDER_SCALE,
});
// Store file metadata and UInt8Array in form data
form.setValue('documentData', {
name: file.name,
type: file.type,
size: file.size,
data: uint8Array, // Store as UInt8Array
images: pdfImages,
});
// Auto-populate title if it's empty
@@ -144,7 +151,7 @@ export const ConfigureDocumentUpload = ({ isSubmitting = false }: ConfigureDocum
<div
{...getRootProps()}
className={cn(
'border-border bg-background relative flex min-h-[160px] cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed transition',
'relative flex min-h-[160px] cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed border-border bg-background transition',
{
'border-primary/50 bg-primary/5': isDragActive,
'hover:bg-muted/30':
@@ -193,21 +200,21 @@ export const ConfigureDocumentUpload = ({ isSubmitting = false }: ConfigureDocum
</FormControl>
{isLoading && (
<div className="bg-background/50 absolute inset-0 flex items-center justify-center rounded-lg">
<Loader className="text-muted-foreground h-10 w-10 animate-spin" />
<div className="absolute inset-0 flex items-center justify-center rounded-lg bg-background/50">
<Loader className="h-10 w-10 animate-spin text-muted-foreground" />
</div>
)}
</div>
) : (
<div className="mt-2 rounded-lg border p-4">
<div className="flex items-center gap-x-4">
<div className="bg-primary/10 text-primary flex h-12 w-12 items-center justify-center rounded-md">
<div className="flex h-12 w-12 items-center justify-center rounded-md bg-primary/10 text-primary">
<FileText className="h-6 w-6" />
</div>
<div className="flex-1">
<div className="text-sm font-medium">{documentData.name}</div>
<div className="text-muted-foreground text-xs">
<div className="text-xs text-muted-foreground">
{formatFileSize(documentData.size)}
</div>
</div>
@@ -46,6 +46,13 @@ export const ZConfigureEmbedFormSchema = z.object({
type: z.string(),
size: z.number(),
data: z.instanceof(Uint8Array), // UInt8Array can't be directly validated by zod
images: z
.object({
width: z.number(),
height: z.number(),
image: z.string(),
})
.array(),
})
.optional(),
});
@@ -5,7 +5,6 @@ import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { EnvelopeItem, FieldType } from '@prisma/client';
import { ReadStatus, type Recipient, SendStatus, SigningStatus } from '@prisma/client';
import { base64 } from '@scure/base';
import { ChevronsUpDown } from 'lucide-react';
import { useFieldArray, useForm } from 'react-hook-form';
import { useHotkeys } from 'react-hotkeys-hook';
@@ -16,6 +15,7 @@ import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { type TFieldMetaSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { nanoid } from '@documenso/lib/universal/id';
import { ADVANCED_FIELD_TYPES_WITH_OPTIONAL_SETTING } from '@documenso/lib/utils/advanced-fields-helpers';
import { PDFViewer } from '@documenso/ui/components/pdf-viewer/pdf-viewer';
import { useRecipientColors } from '@documenso/ui/lib/recipient-colors';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@@ -24,7 +24,6 @@ import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/type
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { FieldSelector } from '@documenso/ui/primitives/field-selector';
import { Form } from '@documenso/ui/primitives/form/form';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { RecipientSelector } from '@documenso/ui/primitives/recipient-selector';
import { Sheet, SheetContent, SheetTrigger } from '@documenso/ui/primitives/sheet';
import { useToast } from '@documenso/ui/primitives/use-toast';
@@ -84,7 +83,7 @@ export const ConfigureFieldsView = ({
};
}, []);
const normalizedDocumentData = useMemo(() => {
const overrideImages = useMemo(() => {
if (envelopeItem) {
return undefined;
}
@@ -93,7 +92,7 @@ export const ConfigureFieldsView = ({
return undefined;
}
return base64.encode(configData.documentData.data);
return configData.documentData.images;
}, [configData.documentData]);
const normalizedEnvelopeItem = useMemo(() => {
@@ -179,8 +178,6 @@ export const ConfigureFieldsView = ({
name: 'fields',
});
const highestPageNumber = Math.max(...localFields.map((field) => field.pageNumber));
const onFieldCopy = useCallback(
(event?: KeyboardEvent | null, options?: { duplicate?: boolean; duplicateAll?: boolean }) => {
const { duplicate = false, duplicateAll = false } = options ?? {};
@@ -548,17 +545,16 @@ export const ConfigureFieldsView = ({
<Form {...form}>
<div>
<PDFViewerLazy
<PDFViewer
presignToken={presignToken}
overrideData={normalizedDocumentData}
overrideImages={overrideImages}
envelopeItem={normalizedEnvelopeItem}
token={undefined}
version="signed"
version="current"
scrollParentRef="window"
/>
<ElementVisible
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}
>
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
{localFields.map((field, index) => {
const recipientIndex = recipients.findIndex((r) => r.id === field.recipientId);
@@ -3,7 +3,7 @@ import { Loader } from 'lucide-react';
export const EmbedClientLoading = () => {
return (
<div className="bg-background fixed left-0 top-0 z-[9999] flex h-full w-full items-center justify-center">
<div className="fixed left-0 top-0 z-[9999] flex h-full w-full items-center justify-center bg-background">
<Loader className="mr-2 h-4 w-4 animate-spin" />
<span>
@@ -31,11 +31,11 @@ import type {
TSignFieldWithTokenMutationSchema,
} from '@documenso/trpc/server/field-router/schema';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { PDFViewer } from '@documenso/ui/components/pdf-viewer/pdf-viewer';
import { Button } from '@documenso/ui/primitives/button';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
@@ -101,8 +101,6 @@ export const EmbedDirectTemplateClientPage = ({
localFields.filter((field) => field.inserted),
];
const highestPendingPageNumber = Math.max(...pendingFields.map((field) => field.page));
const hasSignatureField = localFields.some((field) => isSignatureFieldType(field.type));
const signatureValid = !hasSignatureField || (signature && signature.trim() !== '');
@@ -341,10 +339,11 @@ export const EmbedDirectTemplateClientPage = ({
<div className="relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
{/* Viewer */}
<div className="flex-1">
<PDFViewerLazy
<PDFViewer
envelopeItem={envelopeItems[0]}
token={recipient.token}
version="signed"
version="current"
scrollParentRef="window"
onDocumentLoad={() => setHasDocumentLoaded(true)}
/>
</div>
@@ -478,9 +477,7 @@ export const EmbedDirectTemplateClientPage = ({
</div>
</div>
<ElementVisible
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPendingPageNumber}"]`}
>
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
{showPendingFieldTooltip && pendingFields.length > 0 && (
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning">
<Trans>Click to insert field</Trans>
@@ -50,10 +50,8 @@ export const EmbedDocumentFields = ({
onSignField,
onUnsignField,
}: EmbedDocumentFieldsProps) => {
const highestPageNumber = Math.max(...fields.map((field) => field.page));
return (
<ElementVisible target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}>
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
{fields.map((field) =>
match(field.type)
.with(FieldType.SIGNATURE, () => (
@@ -19,11 +19,11 @@ import {
DocumentReadOnlyFields,
} from '@documenso/ui/components/document/document-read-only-fields';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { PDFViewer } from '@documenso/ui/components/pdf-viewer/pdf-viewer';
import { Button } from '@documenso/ui/primitives/button';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
@@ -106,8 +106,6 @@ export const EmbedSignDocumentV1ClientPage = ({
fields.filter((field) => field.inserted),
];
const highestPendingPageNumber = Math.max(...pendingFields.map((field) => field.page));
const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } =
trpc.recipient.completeDocumentWithToken.useMutation();
@@ -287,10 +285,11 @@ export const EmbedSignDocumentV1ClientPage = ({
<div className="embed--DocumentContainer relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
{/* Viewer */}
<div className="embed--DocumentViewer flex-1">
<PDFViewerLazy
<PDFViewer
envelopeItem={envelopeItems[0]}
token={token}
version="signed"
version="current"
scrollParentRef="window"
onDocumentLoad={() => setHasDocumentLoaded(true)}
/>
</div>
@@ -491,9 +490,7 @@ export const EmbedSignDocumentV1ClientPage = ({
</div>
</div>
<ElementVisible
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPendingPageNumber}"]`}
>
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
{showPendingFieldTooltip && pendingFields.length > 0 && (
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning">
<Trans>Click to insert field</Trans>
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useRef, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
@@ -17,12 +17,12 @@ import type {
} from '@documenso/trpc/server/field-router/schema';
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { PDFViewer } from '@documenso/ui/components/pdf-viewer/pdf-viewer';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
@@ -66,6 +66,8 @@ export const MultiSignDocumentSigningView = ({
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [isExpanded, setIsExpanded] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
@@ -93,8 +95,6 @@ export const MultiSignDocumentSigningView = ({
[],
];
const highestPendingPageNumber = Math.max(...pendingFields.map((field) => field.page));
const uninsertedFields = document?.fields.filter((field) => !field.inserted) ?? [];
const onSignField = async (payload: TSignFieldWithTokenMutationSchema) => {
@@ -179,7 +179,11 @@ export const MultiSignDocumentSigningView = ({
return (
<div className="min-h-screen overflow-hidden bg-background">
<div id="document-field-portal-root" className="relative h-full w-full overflow-y-auto p-8">
<div
id="document-field-portal-root"
ref={scrollContainerRef}
className="relative h-full w-full overflow-y-auto p-8"
>
{match({ isLoading, document })
.with({ isLoading: true }, () => (
<div className="flex min-h-[400px] w-full items-center justify-center">
@@ -226,10 +230,11 @@ export const MultiSignDocumentSigningView = ({
'md:mx-auto md:max-w-2xl': document.status === DocumentStatus.COMPLETED,
})}
>
<PDFViewerLazy
<PDFViewer
envelopeItem={document.envelopeItems[0]}
token={token}
version="signed"
version="current"
scrollParentRef={scrollContainerRef}
onDocumentLoad={() => {
setHasDocumentLoaded(true);
onDocumentReady?.();
@@ -363,9 +368,7 @@ export const MultiSignDocumentSigningView = ({
</div>
{hasDocumentLoaded && (
<ElementVisible
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPendingPageNumber}"]`}
>
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
{showPendingFieldTooltip && pendingFields.length > 0 && (
<FieldToolTip
key={pendingFields[0].id}
@@ -97,13 +97,13 @@ export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps
{menuNavigationLinks.map(({ href, text }) => (
<Link
key={href}
className="text-foreground hover:text-foreground/80 flex items-center gap-2 text-2xl font-semibold"
className="flex items-center gap-2 text-2xl font-semibold text-foreground hover:text-foreground/80"
to={href}
onClick={() => handleMenuItemClick()}
>
{text}
{href === '/inbox' && unreadCountData && unreadCountData.count > 0 && (
<span className="bg-primary text-primary-foreground flex h-6 min-w-[1.5rem] items-center justify-center rounded-full px-1.5 text-xs font-semibold">
<span className="flex h-6 min-w-[1.5rem] items-center justify-center rounded-full bg-primary px-1.5 text-xs font-semibold text-primary-foreground">
{unreadCountData.count > 99 ? '99+' : unreadCountData.count}
</span>
)}
@@ -111,7 +111,7 @@ export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps
))}
<button
className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
className="text-2xl font-semibold text-foreground hover:text-foreground/80"
onClick={async () => authClient.signOut()}
>
<Trans>Sign Out</Trans>
@@ -123,7 +123,7 @@ export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps
<ThemeSwitcher />
</div>
<p className="text-muted-foreground text-sm">
<p className="text-sm text-muted-foreground">
© {new Date().getFullYear()} Documenso, Inc.
<br />
<Trans>All rights reserved.</Trans>
@@ -10,10 +10,10 @@ import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-
import type { TTemplate } from '@documenso/lib/types/template';
import { isRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
import { trpc } from '@documenso/trpc/react';
import { PDFViewer } from '@documenso/ui/components/pdf-viewer/pdf-viewer';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { Stepper } from '@documenso/ui/primitives/stepper';
import { useToast } from '@documenso/ui/primitives/use-toast';
@@ -151,11 +151,12 @@ export const DirectTemplatePageView = ({
gradient
>
<CardContent className="p-2">
<PDFViewerLazy
<PDFViewer
key={template.id}
envelopeItem={template.envelopeItems[0]}
token={directTemplateRecipient.token}
version="signed"
version="current"
scrollParentRef="window"
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
/>
</CardContent>
@@ -82,8 +82,6 @@ export const DirectTemplateSigningForm = ({
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const highestPageNumber = Math.max(...localFields.map((field) => field.page));
const fieldsRequiringValidation = useMemo(() => {
return localFields.filter((field) => isFieldUnsignedAndRequired(field));
}, [localFields]);
@@ -250,9 +248,7 @@ export const DirectTemplateSigningForm = ({
<DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} />
<DocumentFlowFormContainerContent>
<ElementVisible
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}
>
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
{validateUninsertedFields && uninsertedFields[0] && (
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
<Trans>Click to insert field</Trans>
@@ -167,7 +167,7 @@ export const DocumentSigningAutoSign = ({ recipient, fields }: DocumentSigningAu
</DialogTitle>
</DialogHeader>
<div className="text-muted-foreground max-w-[50ch]">
<div className="max-w-[50ch] text-muted-foreground">
<p>
<Trans>
When you sign a document, we can automatically fill in and sign the following fields
@@ -27,10 +27,10 @@ import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/fie
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
import { trpc } from '@documenso/trpc/react';
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
import { PDFViewer } from '@documenso/ui/components/pdf-viewer/pdf-viewer';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { DocumentSigningAttachmentsPopover } from '~/components/general/document-signing/document-signing-attachments-popover';
import { DocumentSigningAutoSign } from '~/components/general/document-signing/document-signing-auto-sign';
@@ -162,8 +162,6 @@ export const DocumentSigningPageViewV1 = ({
: undefined;
}, [document.documentMeta?.signingOrder, allRecipients, recipient.id]);
const highestPageNumber = Math.max(...fields.map((field) => field.page));
const pendingFields = fieldsRequiringValidation.filter((field) => !field.inserted);
const hasPendingFields = pendingFields.length > 0;
@@ -274,11 +272,12 @@ export const DocumentSigningPageViewV1 = ({
<div className="flex-1">
<Card className="rounded-xl before:rounded-xl" gradient>
<CardContent className="p-2">
<PDFViewerLazy
<PDFViewer
key={document.envelopeItems[0].id}
envelopeItem={document.envelopeItems[0]}
token={recipient.token}
version="signed"
version="current"
scrollParentRef="window"
/>
</CardContent>
</Card>
@@ -400,9 +399,7 @@ export const DocumentSigningPageViewV1 = ({
<DocumentSigningAutoSign recipient={recipient} fields={fields} />
)}
<ElementVisible
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}
>
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
{fields
.filter(
(field) =>
@@ -1,4 +1,4 @@
import { lazy, useMemo } from 'react';
import { lazy, useMemo, useRef } from 'react';
import { Plural, Trans } from '@lingui/react/macro';
import { EnvelopeType, RecipientRole } from '@prisma/client';
@@ -8,8 +8,9 @@ import { Link } from 'react-router';
import { match } from 'ts-pattern';
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
import { PDF_VIEWER_ERROR_MESSAGES } from '@documenso/lib/constants/pdf-viewer-i18n';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy';
import { EnvelopePdfViewer } from '@documenso/ui/components/pdf-viewer/envelope-pdf-viewer';
import { Button } from '@documenso/ui/primitives/button';
import { Separator } from '@documenso/ui/primitives/separator';
@@ -40,6 +41,8 @@ const EnvelopeSignerPageRenderer = lazy(
export const DocumentSigningPageViewV2 = () => {
const { envelopeItems, currentEnvelopeItem, setCurrentEnvelopeItem } = useCurrentEnvelopeRender();
const scrollableContainerRef = useRef<HTMLDivElement>(null);
const {
isDirectTemplate,
envelope,
@@ -199,7 +202,10 @@ export const DocumentSigningPageViewV2 = () => {
</div>
</div>
<div className="embed--DocumentContainer flex-1 overflow-y-auto">
<div
className="embed--DocumentContainer flex-1 overflow-y-auto"
ref={scrollableContainerRef}
>
<div className="flex flex-col">
{/* Horizontal envelope item selector */}
{envelopeItems.length > 1 && (
@@ -228,15 +234,16 @@ export const DocumentSigningPageViewV2 = () => {
{/* Document View */}
<div className="embed--DocumentViewer flex flex-col items-center justify-center p-2 sm:mt-4 sm:p-4">
{currentEnvelopeItem ? (
<PDFViewerKonvaLazy
renderer="signing"
<EnvelopePdfViewer
key={currentEnvelopeItem.id}
customPageRenderer={EnvelopeSignerPageRenderer}
scrollParentRef={scrollableContainerRef}
errorMessage={PDF_VIEWER_ERROR_MESSAGES.signing}
/>
) : (
<div className="flex flex-col items-center justify-center py-32">
<p className="text-sm text-foreground">
<Trans>No documents found</Trans>
<Trans>No document selected</Trans>
</p>
</div>
)}
@@ -1,4 +1,4 @@
import { lazy, useEffect, useState } from 'react';
import { lazy, useEffect, useRef, useState } from 'react';
import { Trans } from '@lingui/react/macro';
import { type DocumentData, DocumentStatus, type EnvelopeItem, EnvelopeType } from '@prisma/client';
@@ -9,9 +9,11 @@ import {
EnvelopeRenderProvider,
useCurrentEnvelopeRender,
} from '@documenso/lib/client-only/providers/envelope-render-provider';
import { PDF_VIEWER_ERROR_MESSAGES } from '@documenso/lib/constants/pdf-viewer-i18n';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy';
import { EnvelopePdfViewer } from '@documenso/ui/components/pdf-viewer/envelope-pdf-viewer';
import { PDFViewer } from '@documenso/ui/components/pdf-viewer/pdf-viewer';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
@@ -21,7 +23,6 @@ import {
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog';
@@ -35,7 +36,7 @@ export type DocumentCertificateQRViewProps = {
documentId: number;
title: string;
internalVersion: number;
envelopeItems: (EnvelopeItem & { documentData: DocumentData })[];
envelopeItems: (EnvelopeItem & { documentData: Omit<DocumentData, 'metadata'> })[];
documentTeamUrl: string;
recipientCount?: number;
completedDate?: Date;
@@ -104,11 +105,13 @@ export const DocumentCertificateQRView = ({
{internalVersion === 2 ? (
<EnvelopeRenderProvider
version="current"
envelope={{
envelopeItems,
id: envelopeItems[0].envelopeId,
status: DocumentStatus.COMPLETED,
type: EnvelopeType.DOCUMENT,
}}
envelopeItems={envelopeItems}
token={token}
>
<DocumentCertificateQrV2
@@ -149,11 +152,12 @@ export const DocumentCertificateQRView = ({
</div>
<div className="mt-12 w-full">
<PDFViewerLazy
<PDFViewer
key={envelopeItems[0].id}
envelopeItem={envelopeItems[0]}
token={token}
version="signed"
version="current"
scrollParentRef="window"
/>
</div>
</>
@@ -175,7 +179,9 @@ const DocumentCertificateQrV2 = ({
formattedDate,
token,
}: DocumentCertificateQrV2Props) => {
const { currentEnvelopeItem, envelopeItems } = useCurrentEnvelopeRender();
const { envelopeItems } = useCurrentEnvelopeRender();
const scrollableContainerRef = useRef<HTMLDivElement>(null);
return (
<div className="flex min-h-screen flex-col items-start">
@@ -207,10 +213,14 @@ const DocumentCertificateQrV2 = ({
/>
</div>
<div className="mt-12 w-full">
<div className="mt-12 max-h-[80vh] w-full overflow-y-auto" ref={scrollableContainerRef}>
<EnvelopeRendererFileSelector className="mb-4 p-0" fields={[]} secondaryOverride={''} />
<PDFViewerKonvaLazy renderer="preview" customPageRenderer={EnvelopeGenericPageRenderer} />
<EnvelopePdfViewer
customPageRenderer={EnvelopeGenericPageRenderer}
scrollParentRef={scrollableContainerRef}
errorMessage={PDF_VIEWER_ERROR_MESSAGES.preview}
/>
</div>
</div>
);
@@ -15,6 +15,7 @@ import {
import type { TDocument } from '@documenso/lib/types/document';
import { ZDocumentAccessAuthTypesSchema } from '@documenso/lib/types/document-auth';
import { trpc } from '@documenso/trpc/react';
import { PDFViewer } from '@documenso/ui/components/pdf-viewer/pdf-viewer';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { AddFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add-fields';
@@ -27,7 +28,6 @@ import { AddSubjectFormPartial } from '@documenso/ui/primitives/document-flow/ad
import type { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types';
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { Stepper } from '@documenso/ui/primitives/stepper';
import { useToast } from '@documenso/ui/primitives/use-toast';
@@ -440,11 +440,12 @@ export const DocumentEditForm = ({
gradient
>
<CardContent className="p-2">
<PDFViewerLazy
<PDFViewer
key={document.envelopeItems[0].id}
envelopeItem={document.envelopeItems[0]}
token={undefined}
version="signed"
version="current"
scrollParentRef="window"
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
/>
</CardContent>
@@ -175,15 +175,6 @@ export const EnvelopeEditorFieldDragDrop = ({
const { top, left, height, width } = getBoundingClientRect($page);
console.log({
top,
left,
height,
width,
rawPageX: event.pageX,
rawPageY: event.pageY,
});
const pageNumber = parseInt($page.getAttribute('data-page-number') ?? '1', 10);
// Calculate x and y as a percentage of the page width and height
@@ -278,13 +269,13 @@ export const EnvelopeEditorFieldDragDrop = ({
onMouseDown={() => setSelectedField(field.type)}
data-selected={selectedField === field.type ? true : undefined}
className={cn(
'border-border group flex h-12 cursor-pointer items-center justify-center rounded-lg border px-4 transition-colors',
'group flex h-12 cursor-pointer items-center justify-center rounded-lg border border-border px-4 transition-colors',
RECIPIENT_COLOR_STYLES[selectedRecipientColor].fieldButton,
)}
>
<p
className={cn(
'text-muted-foreground font-noto group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
'flex items-center justify-center gap-x-1.5 font-noto text-sm font-normal text-muted-foreground group-data-[selected]:text-foreground',
field.className,
{
'group-hover:text-recipient-green': selectedRecipientColor === 'green',
@@ -306,7 +297,7 @@ export const EnvelopeEditorFieldDragDrop = ({
{selectedField && (
<div
className={cn(
'text-muted-foreground dark:text-muted-background font-noto pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center rounded-[2px] bg-white ring-2 transition duration-200 [container-type:size]',
'dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center rounded-[2px] bg-white font-noto text-muted-foreground ring-2 transition duration-200 [container-type:size]',
RECIPIENT_COLOR_STYLES[selectedRecipientColor].base,
selectedField === FieldType.SIGNATURE && 'font-signature',
{
@@ -10,7 +10,10 @@ import { CopyPlusIcon, SquareStackIcon, TrashIcon, UserCircleIcon } from 'lucide
import type { TLocalField } from '@documenso/lib/client-only/hooks/use-editor-fields';
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
import {
type PageRenderData,
useCurrentEnvelopeRender,
} from '@documenso/lib/client-only/providers/envelope-render-provider';
import { FIELD_META_DEFAULT_VALUES } from '@documenso/lib/types/field-meta';
import {
MIN_FIELD_HEIGHT_PX,
@@ -22,10 +25,15 @@ import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
import { CommandDialog } from '@documenso/ui/primitives/command';
import { EnvelopePageImage } from '../envelope/envelope-page-image';
import { fieldButtonList } from './envelope-editor-fields-drag-drop';
import { EnvelopeRecipientSelectorCommand } from './envelope-recipient-selector';
export default function EnvelopeEditorFieldsPageRenderer() {
export default function EnvelopeEditorFieldsPageRenderer({
pageData,
}: {
pageData: PageRenderData;
}) {
const { t, i18n } = useLingui();
const { envelope, editorFields, getRecipientColorKey } = useCurrentEnvelopeEditor();
const { currentEnvelopeItem, setRenderError } = useCurrentEnvelopeRender();
@@ -40,31 +48,24 @@ export default function EnvelopeEditorFieldsPageRenderer() {
const {
stage,
pageLayer,
canvasElement,
konvaContainer,
pageContext,
scaledViewport,
unscaledViewport,
} = usePageRenderer(({ stage, pageLayer }) => createPageCanvas(stage, pageLayer));
renderStatus,
imageProps,
} = usePageRenderer(({ stage, pageLayer }) => createPageCanvas(stage, pageLayer), pageData);
const { _className, scale } = pageContext;
const { scale, pageNumber } = pageData;
const localPageFields = useMemo(
() =>
editorFields.localFields.filter(
(field) =>
field.page === pageContext.pageNumber && field.envelopeItemId === currentEnvelopeItem?.id,
(field) => field.page === pageNumber && field.envelopeItemId === currentEnvelopeItem?.id,
),
[editorFields.localFields, pageContext.pageNumber],
[editorFields.localFields, pageNumber, currentEnvelopeItem?.id],
);
const handleResizeOrMove = (event: KonvaEventObject<Event>) => {
const { current: container } = canvasElement;
if (!container) {
return;
}
const isDragEvent = event.type === 'dragend';
const fieldGroup = event.target as Konva.Group;
@@ -344,7 +345,6 @@ export default function EnvelopeEditorFieldsPageRenderer() {
// Create a field if no items are selected or the size is too small.
if (
selectedFieldGroups.length === 0 &&
canvasElement.current &&
unscaledBoxWidth > MIN_FIELD_WIDTH_PX &&
unscaledBoxHeight > MIN_FIELD_HEIGHT_PX &&
editorFields.selectedRecipient &&
@@ -531,7 +531,7 @@ export default function EnvelopeEditorFieldsPageRenderer() {
removePendingField();
if (!canvasElement.current || !currentEnvelopeItem || !editorFields.selectedRecipient) {
if (!currentEnvelopeItem || !editorFields.selectedRecipient) {
return;
}
@@ -546,7 +546,7 @@ export default function EnvelopeEditorFieldsPageRenderer() {
editorFields.addField({
envelopeItemId: currentEnvelopeItem.id,
page: pageContext.pageNumber,
page: pageNumber,
type,
positionX: fieldX,
positionY: fieldY,
@@ -575,10 +575,7 @@ export default function EnvelopeEditorFieldsPageRenderer() {
}
return (
<div
className="relative w-full"
key={`${currentEnvelopeItem.id}-renderer-${pageContext.pageNumber}`}
>
<div className="relative w-full" key={`${currentEnvelopeItem.id}-renderer-${pageNumber}`}>
{selectedKonvaFieldGroups.length > 0 &&
interactiveTransformer.current &&
!isFieldChanging && (
@@ -641,13 +638,7 @@ export default function EnvelopeEditorFieldsPageRenderer() {
{/* The element Konva will inject it's canvas into. */}
<div className="konva-container absolute inset-0 z-10 w-full" ref={konvaContainer}></div>
{/* Canvas the PDF will be rendered on. */}
<canvas
className={`${_className}__canvas z-0`}
ref={canvasElement}
height={scaledViewport.height}
width={scaledViewport.width}
/>
<EnvelopePageImage renderStatus={renderStatus} imageProps={imageProps} />
</div>
);
}
@@ -1,4 +1,4 @@
import { lazy, useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
@@ -12,6 +12,7 @@ import { match } from 'ts-pattern';
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
import { PDF_VIEWER_ERROR_MESSAGES } from '@documenso/lib/constants/pdf-viewer-i18n';
import type { NormalizedFieldWithContext } from '@documenso/lib/server-only/ai/envelope/detect-fields/types';
import {
FIELD_META_DEFAULT_VALUES,
@@ -29,7 +30,7 @@ import {
} from '@documenso/lib/types/field-meta';
import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy';
import { EnvelopePdfViewer } from '@documenso/ui/components/pdf-viewer/envelope-pdf-viewer';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import { Separator } from '@documenso/ui/primitives/separator';
@@ -49,13 +50,10 @@ import { EditorFieldTextForm } from '~/components/forms/editor/editor-field-text
import { useCurrentTeam } from '~/providers/team';
import { EnvelopeEditorFieldDragDrop } from './envelope-editor-fields-drag-drop';
import EnvelopeEditorFieldsPageRenderer from './envelope-editor-fields-page-renderer';
import { EnvelopeRendererFileSelector } from './envelope-file-selector';
import { EnvelopeRecipientSelector } from './envelope-recipient-selector';
const EnvelopeEditorFieldsPageRenderer = lazy(
async () => import('~/components/general/envelope-editor/envelope-editor-fields-page-renderer'),
);
const FieldSettingsTypeTranslations: Record<FieldType, MessageDescriptor> = {
[FieldType.SIGNATURE]: msg`Signature Settings`,
[FieldType.FREE_SIGNATURE]: msg`Free Signature Settings`,
@@ -75,6 +73,8 @@ export const EnvelopeEditorFieldsPage = () => {
const team = useCurrentTeam();
const scrollableContainerRef = useRef<HTMLDivElement>(null);
const { envelope, editorFields, relativePath } = useCurrentEnvelopeEditor();
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
@@ -156,12 +156,12 @@ export const EnvelopeEditorFieldsPage = () => {
return (
<div className="relative flex h-full">
<div className="flex w-full flex-col overflow-y-auto">
<div className="flex h-full w-full flex-col overflow-y-auto" ref={scrollableContainerRef}>
{/* Horizontal envelope item selector */}
<EnvelopeRendererFileSelector fields={editorFields.localFields} />
{/* Document View */}
<div className="mt-4 flex flex-col items-center justify-center">
<div className="mt-4 flex h-full flex-col items-center justify-center">
{envelope.recipients.length === 0 && (
<Alert
variant="neutral"
@@ -185,9 +185,10 @@ export const EnvelopeEditorFieldsPage = () => {
)}
{currentEnvelopeItem !== null ? (
<PDFViewerKonvaLazy
renderer="editor"
<EnvelopePdfViewer
customPageRenderer={EnvelopeEditorFieldsPageRenderer}
scrollParentRef={scrollableContainerRef}
errorMessage={PDF_VIEWER_ERROR_MESSAGES.editor}
/>
) : (
<div className="flex flex-col items-center justify-center py-32">
@@ -1,4 +1,4 @@
import { lazy, useEffect, useMemo, useState } from 'react';
import { lazy, useEffect, useMemo, useRef, useState } from 'react';
import { faker } from '@faker-js/faker/locale/en';
import { Trans } from '@lingui/react/macro';
@@ -11,12 +11,13 @@ import {
EnvelopeRenderProvider,
useCurrentEnvelopeRender,
} from '@documenso/lib/client-only/providers/envelope-render-provider';
import { PDF_VIEWER_ERROR_MESSAGES } from '@documenso/lib/constants/pdf-viewer-i18n';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { extractFieldInsertionValues } from '@documenso/lib/utils/envelope-signing';
import { toCheckboxCustomText } from '@documenso/lib/utils/fields';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy';
import { EnvelopePdfViewer } from '@documenso/ui/components/pdf-viewer/envelope-pdf-viewer';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { RecipientSelector } from '@documenso/ui/primitives/recipient-selector';
import { Separator } from '@documenso/ui/primitives/separator';
@@ -33,6 +34,8 @@ export const EnvelopeEditorPreviewPage = () => {
const { currentEnvelopeItem, fields } = useCurrentEnvelopeRender();
const scrollableContainerRef = useRef<HTMLDivElement>(null);
const [selectedPreviewMode, setSelectedPreviewMode] = useState<'recipient' | 'signed'>(
'recipient',
);
@@ -200,7 +203,9 @@ export const EnvelopeEditorPreviewPage = () => {
// Override the parent renderer provider so we can inject custom fields.
return (
<EnvelopeRenderProvider
version="current"
envelope={envelope}
envelopeItems={envelope.envelopeItems}
token={undefined}
fields={fieldsWithPlaceholders}
recipients={envelope.recipients.map((recipient) => ({
@@ -212,12 +217,12 @@ export const EnvelopeEditorPreviewPage = () => {
}}
>
<div className="relative flex h-full">
<div className="flex w-full flex-col overflow-y-auto">
<div className="flex w-full flex-col overflow-y-auto" ref={scrollableContainerRef}>
{/* Horizontal envelope item selector */}
<EnvelopeRendererFileSelector fields={editorFields.localFields} />
{/* Document View */}
<div className="mt-4 flex flex-col items-center justify-center">
<div className="mt-4 flex h-full flex-col items-center justify-center">
<Alert variant="warning" className="mb-4 max-w-[800px]">
<AlertTitle>
<Trans>Preview Mode</Trans>
@@ -228,9 +233,10 @@ export const EnvelopeEditorPreviewPage = () => {
</Alert>
{currentEnvelopeItem !== null ? (
<PDFViewerKonvaLazy
renderer="editor"
<EnvelopePdfViewer
customPageRenderer={EnvelopeGenericPageRenderer}
scrollParentRef={scrollableContainerRef}
errorMessage={PDF_VIEWER_ERROR_MESSAGES.preview}
/>
) : (
<div className="flex flex-col items-center justify-center py-32">
@@ -5,17 +5,22 @@ import { DocumentStatus, type Recipient, SigningStatus } from '@prisma/client';
import type Konva from 'konva';
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
import {
type PageRenderData,
useCurrentEnvelopeRender,
} from '@documenso/lib/client-only/providers/envelope-render-provider';
import type { TEnvelope } from '@documenso/lib/types/envelope';
import { renderField } from '@documenso/lib/universal/field-renderer/render-field';
import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
import { EnvelopeRecipientFieldTooltip } from '@documenso/ui/components/document/envelope-recipient-field-tooltip';
import { EnvelopePageImage } from '../envelope/envelope-page-image';
type GenericLocalField = TEnvelope['fields'][number] & {
recipient: Pick<Recipient, 'id' | 'name' | 'email' | 'signingStatus'>;
};
export default function EnvelopeGenericPageRenderer() {
export default function EnvelopeGenericPageRenderer({ pageData }: { pageData: PageRenderData }) {
const { i18n } = useLingui();
const {
@@ -28,19 +33,12 @@ export default function EnvelopeGenericPageRenderer() {
overrideSettings,
} = useCurrentEnvelopeRender();
const {
stage,
pageLayer,
canvasElement,
konvaContainer,
pageContext,
scaledViewport,
unscaledViewport,
} = usePageRenderer(({ stage, pageLayer }) => {
createPageCanvas(stage, pageLayer);
});
const { stage, pageLayer, imageProps, konvaContainer, unscaledViewport, renderStatus } =
usePageRenderer(({ stage, pageLayer }) => {
createPageCanvas(stage, pageLayer);
}, pageData);
const { _className, scale } = pageContext;
const { scale, pageNumber } = pageData;
const localPageFields = useMemo((): GenericLocalField[] => {
if (envelopeStatus === DocumentStatus.COMPLETED) {
@@ -49,8 +47,7 @@ export default function EnvelopeGenericPageRenderer() {
return fields
.filter(
(field) =>
field.page === pageContext.pageNumber && field.envelopeItemId === currentEnvelopeItem?.id,
(field) => field.page === pageNumber && field.envelopeItemId === currentEnvelopeItem?.id,
)
.map((field) => {
const recipient = recipients.find((recipient) => recipient.id === field.recipientId);
@@ -73,7 +70,7 @@ export default function EnvelopeGenericPageRenderer() {
(recipient.signingStatus === SigningStatus.SIGNED ? inserted : true) ||
fieldMeta?.readOnly,
);
}, [fields, pageContext.pageNumber, currentEnvelopeItem?.id, recipients]);
}, [fields, pageNumber, currentEnvelopeItem?.id, recipients]);
const unsafeRenderFieldOnLayer = (field: GenericLocalField) => {
if (!pageLayer.current) {
@@ -160,10 +157,7 @@ export default function EnvelopeGenericPageRenderer() {
}
return (
<div
className="relative w-full"
key={`${currentEnvelopeItem.id}-renderer-${pageContext.pageNumber}`}
>
<div className="relative w-full" key={`${currentEnvelopeItem.id}-renderer-${pageNumber}`}>
{overrideSettings?.showRecipientTooltip &&
localPageFields.map((field) => (
<EnvelopeRecipientFieldTooltip
@@ -177,13 +171,7 @@ export default function EnvelopeGenericPageRenderer() {
{/* The element Konva will inject it's canvas into. */}
<div className="konva-container absolute inset-0 z-10 w-full" ref={konvaContainer}></div>
{/* Canvas the PDF will be rendered on. */}
<canvas
className={`${_className}__canvas z-0`}
ref={canvasElement}
height={scaledViewport.height}
width={scaledViewport.width}
/>
<EnvelopePageImage renderStatus={renderStatus} imageProps={imageProps} />
</div>
);
}
@@ -14,7 +14,10 @@ import type { KonvaEventObject } from 'konva/lib/Node';
import { match } from 'ts-pattern';
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
import {
type PageRenderData,
useCurrentEnvelopeRender,
} from '@documenso/lib/client-only/providers/envelope-render-provider';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { DIRECT_TEMPLATE_RECIPIENT_EMAIL } from '@documenso/lib/constants/direct-templates';
import { isBase64Image } from '@documenso/lib/constants/signatures';
@@ -44,12 +47,13 @@ import { handleTextFieldClick } from '~/utils/field-signing/text-field';
import { useRequiredDocumentSigningAuthContext } from '../document-signing/document-signing-auth-provider';
import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider';
import { EnvelopePageImage } from '../envelope/envelope-page-image';
type GenericLocalField = TEnvelope['fields'][number] & {
recipient: Pick<Recipient, 'id' | 'name' | 'email' | 'signingStatus'>;
};
export default function EnvelopeSignerPageRenderer() {
export default function EnvelopeSignerPageRenderer({ pageData }: { pageData: PageRenderData }) {
const { t, i18n } = useLingui();
const { currentEnvelopeItem, setRenderError } = useCurrentEnvelopeRender();
const { sessionData } = useOptionalSession();
@@ -77,17 +81,10 @@ export default function EnvelopeSignerPageRenderer() {
const { onFieldSigned, onFieldUnsigned } = useEmbedSigningContext() || {};
const {
stage,
pageLayer,
canvasElement,
konvaContainer,
pageContext,
scaledViewport,
unscaledViewport,
} = usePageRenderer(({ stage, pageLayer }) => createPageCanvas(stage, pageLayer));
const { stage, pageLayer, imageProps, konvaContainer, unscaledViewport, renderStatus } =
usePageRenderer(({ stage, pageLayer }) => createPageCanvas(stage, pageLayer), pageData);
const { _className, scale } = pageContext;
const { scale, pageNumber } = pageData;
const { envelope } = envelopeData;
@@ -99,10 +96,9 @@ export default function EnvelopeSignerPageRenderer() {
}
return fieldsToRender.filter(
(field) =>
field.page === pageContext.pageNumber && field.envelopeItemId === currentEnvelopeItem?.id,
(field) => field.page === pageNumber && field.envelopeItemId === currentEnvelopeItem?.id,
);
}, [recipientFields, selectedAssistantRecipientFields, pageContext.pageNumber]);
}, [recipientFields, selectedAssistantRecipientFields, pageNumber, currentEnvelopeItem?.id]);
/**
* Returns fields that have been fully signed by other recipients for this specific
@@ -117,7 +113,7 @@ export default function EnvelopeSignerPageRenderer() {
return recipient.fields
.filter(
(field) =>
field.page === pageContext.pageNumber &&
field.page === pageNumber &&
field.envelopeItemId === currentEnvelopeItem?.id &&
(field.inserted || field.fieldMeta?.readOnly),
)
@@ -132,7 +128,7 @@ export default function EnvelopeSignerPageRenderer() {
},
}));
});
}, [envelope.recipients, pageContext.pageNumber]);
}, [envelope.recipients, pageNumber]);
const unsafeRenderFieldOnLayer = (unparsedField: Field & { signature?: Signature | null }) => {
if (!pageLayer.current) {
@@ -534,14 +530,11 @@ export default function EnvelopeSignerPageRenderer() {
}
return (
<div
className="relative w-full"
key={`${currentEnvelopeItem.id}-renderer-${pageContext.pageNumber}`}
>
<div className="relative w-full" key={`${currentEnvelopeItem.id}-renderer-${pageNumber}`}>
{showPendingFieldTooltip &&
recipientFieldsRemaining.length > 0 &&
recipientFieldsRemaining[0]?.envelopeItemId === currentEnvelopeItem?.id &&
recipientFieldsRemaining[0]?.page === pageContext.pageNumber && (
recipientFieldsRemaining[0]?.page === pageNumber && (
<EnvelopeFieldToolTip
key={recipientFieldsRemaining[0].id}
field={recipientFieldsRemaining[0]}
@@ -563,13 +556,7 @@ export default function EnvelopeSignerPageRenderer() {
{/* The element Konva will inject it's canvas into. */}
<div className="konva-container absolute inset-0 z-10 w-full" ref={konvaContainer}></div>
{/* Canvas the PDF will be rendered on. */}
<canvas
className={`${_className}__canvas z-0`}
ref={canvasElement}
height={scaledViewport.height}
width={scaledViewport.width}
/>
<EnvelopePageImage renderStatus={renderStatus} imageProps={imageProps} />
</div>
);
}
@@ -71,6 +71,14 @@ export const EnvelopeSignerCompleteDialog = () => {
if (fieldTooltip) {
fieldTooltip.scrollIntoView({ behavior: 'smooth', block: 'center' });
} else {
// Tooltip not in DOM (page virtualized away) — signal the PDF viewer
// to scroll to the correct page via the data attribute.
const pdfContent = document.querySelector('[data-pdf-content]');
if (pdfContent) {
pdfContent.setAttribute('data-scroll-to-page', String(nextField.page));
}
}
},
isEnvelopeItemSwitch ? 150 : 50,
@@ -0,0 +1,32 @@
import { Trans } from '@lingui/react/macro';
import { Spinner } from '@documenso/ui/primitives/spinner';
type EnvelopePageImageProps = {
renderStatus: 'loading' | 'loaded' | 'error';
imageProps: React.ImgHTMLAttributes<HTMLImageElement> & Record<string, unknown> & { alt: '' };
};
export const EnvelopePageImage = ({ renderStatus, imageProps }: EnvelopePageImageProps) => {
return (
<>
{/* Loading State */}
{renderStatus === 'loading' && (
<div className="absolute inset-0 z-10 flex items-center justify-center">
<Spinner />
</div>
)}
{renderStatus === 'error' && (
<div className="absolute inset-0 z-10 flex items-center justify-center">
<p>
<Trans>Error loading page</Trans>
</p>
</div>
)}
{/* The PDF image. */}
<img {...imageProps} alt="" />
</>
);
};
@@ -14,11 +14,11 @@ import {
import { ZDocumentAccessAuthTypesSchema } from '@documenso/lib/types/document-auth';
import type { TTemplate } from '@documenso/lib/types/template';
import { trpc } from '@documenso/trpc/react';
import { PDFViewer } from '@documenso/ui/components/pdf-viewer/pdf-viewer';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { Stepper } from '@documenso/ui/primitives/stepper';
import { AddTemplateFieldsFormPartial } from '@documenso/ui/primitives/template-flow/add-template-fields';
import type { TAddTemplateFieldsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-fields.types';
@@ -312,11 +312,12 @@ export const TemplateEditForm = ({
gradient
>
<CardContent className="p-2">
<PDFViewerLazy
<PDFViewer
key={template.envelopeItems[0].id}
envelopeItem={template.envelopeItems[0]}
token={undefined}
version="signed"
version="current"
scrollParentRef="window"
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
/>
</CardContent>
@@ -21,17 +21,17 @@ export const UserProfileSkeleton = ({ className, user, rows = 2 }: UserProfileSk
return (
<div
className={cn(
'dark:bg-background flex flex-col items-center rounded-xl bg-neutral-100 p-4',
'flex flex-col items-center rounded-xl bg-neutral-100 p-4 dark:bg-background',
className,
)}
>
<div className="border-border bg-background text-muted-foreground inline-block max-w-full truncate rounded-md border px-2.5 py-1.5 text-sm lowercase">
<div className="inline-block max-w-full truncate rounded-md border border-border bg-background px-2.5 py-1.5 text-sm lowercase text-muted-foreground">
{baseUrl.host}/u/{user.url}
</div>
<div className="mt-4">
<div className="bg-primary/10 rounded-full p-1.5">
<div className="bg-background flex h-20 w-20 items-center justify-center rounded-full border-2">
<div className="rounded-full bg-primary/10 p-1.5">
<div className="flex h-20 w-20 items-center justify-center rounded-full border-2 bg-background">
<User2 className="h-12 w-12 text-[hsl(228,10%,90%)]" />
</div>
</div>
@@ -41,16 +41,16 @@ export const UserProfileSkeleton = ({ className, user, rows = 2 }: UserProfileSk
<div className="flex items-center justify-center gap-x-2">
<h2 className="max-w-[12rem] truncate text-2xl font-semibold">{user.name}</h2>
<VerifiedIcon className="text-primary h-8 w-8" />
<VerifiedIcon className="h-8 w-8 text-primary" />
</div>
<div className="dark:bg-foreground/30 mx-auto mt-4 h-2 w-52 rounded-full bg-neutral-300" />
<div className="dark:bg-foreground/20 mx-auto mt-2 h-2 w-36 rounded-full bg-neutral-200" />
<div className="mx-auto mt-4 h-2 w-52 rounded-full bg-neutral-300 dark:bg-foreground/30" />
<div className="mx-auto mt-2 h-2 w-36 rounded-full bg-neutral-200 dark:bg-foreground/20" />
</div>
<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">
<div className="divide-y-2 divide-neutral-200 overflow-hidden rounded-lg border-2 border-neutral-200 dark:divide-foreground/30 dark:border-foreground/30">
<div className="bg-neutral-50 p-4 font-medium text-muted-foreground dark:bg-foreground/20">
<Trans>Documents</Trans>
</div>
@@ -59,14 +59,14 @@ export const UserProfileSkeleton = ({ className, user, rows = 2 }: UserProfileSk
.map((_, index) => (
<div
key={index}
className="bg-background flex items-center justify-between gap-x-6 p-4"
className="flex items-center justify-between gap-x-6 bg-background p-4"
>
<div className="flex items-center gap-x-2">
<File className="text-muted-foreground/80 h-8 w-8" strokeWidth={1.5} />
<File className="h-8 w-8 text-muted-foreground/80" strokeWidth={1.5} />
<div className="space-y-2">
<div className="dark:bg-foreground/30 h-1.5 w-24 rounded-full bg-neutral-300 md:w-36" />
<div className="dark:bg-foreground/20 h-1.5 w-16 rounded-full bg-neutral-200 md:w-24" />
<div className="h-1.5 w-24 rounded-full bg-neutral-300 md:w-36 dark:bg-foreground/30" />
<div className="h-1.5 w-16 rounded-full bg-neutral-200 md:w-24 dark:bg-foreground/20" />
</div>
</div>
@@ -3,6 +3,7 @@ import {
BarChart3,
Building2Icon,
FileStack,
MailIcon,
Settings,
Trophy,
Users,
@@ -122,6 +123,20 @@ export default function AdminLayout({ loaderData }: Route.ComponentProps) {
</Link>
</Button>
<Button
variant="ghost"
className={cn(
'justify-start md:w-full',
pathname?.startsWith('/admin/email-domains') && 'bg-secondary',
)}
asChild
>
<Link to="/admin/email-domains">
<MailIcon className="mr-2 h-5 w-5" />
<Trans>Email Domains</Trans>
</Link>
</Button>
<Button
variant="ghost"
className={cn(
@@ -0,0 +1,295 @@
import { useMemo } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { EmailDomainStatus } from '@prisma/client';
import { CheckCircle2Icon, ClockIcon, CopyIcon, RotateCcwIcon } from 'lucide-react';
import { DateTime } from 'luxon';
import { Link, redirect } from 'react-router';
import { match } from 'ts-pattern';
import { generateEmailDomainRecords } from '@documenso/lib/utils/email-domains';
import { trpc } from '@documenso/trpc/react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@documenso/ui/primitives/alert-dialog';
import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { useToast } from '@documenso/ui/primitives/use-toast';
import type { Route } from './+types/email-domains.$id';
export function loader({ params }: Route.LoaderArgs) {
const id = params.id;
if (!id) {
throw redirect('/admin/email-domains');
}
return { emailDomainId: id };
}
export default function AdminEmailDomainDetailPage({ loaderData }: Route.ComponentProps) {
const { emailDomainId } = loaderData;
const { _, i18n } = useLingui();
const { toast } = useToast();
const {
data: emailDomain,
isPending: isLoading,
refetch,
} = trpc.admin.emailDomain.get.useQuery({ emailDomainId });
const { mutate: reregisterDomain, isPending: isReregistering } =
trpc.admin.emailDomain.reregister.useMutation({
onSuccess: () => {
toast({
title: _(msg`Domain re-registered`),
description: _(
msg`The SES identity has been deleted and recreated with the same keys. DNS records remain unchanged.`,
),
});
void refetch();
},
onError: () => {
toast({
title: _(msg`Error`),
description: _(msg`Failed to re-register email domain`),
variant: 'destructive',
});
},
});
const dnsRecords = useMemo(() => {
if (!emailDomain) {
return [];
}
return generateEmailDomainRecords(emailDomain.selector, emailDomain.publicKey);
}, [emailDomain]);
const emailColumns = useMemo(() => {
return [
{
header: _(msg`Email`),
accessorKey: 'email',
},
{
header: _(msg`Display Name`),
accessorKey: 'emailName',
},
{
header: _(msg`Created`),
accessorKey: 'createdAt',
cell: ({ row }) => i18n.date(row.original.createdAt),
},
] satisfies DataTableColumnDef<NonNullable<typeof emailDomain>['emails'][number]>[];
}, []);
const onCopyToClipboard = async (text: string) => {
await navigator.clipboard.writeText(text);
toast({
title: _(msg`Copied to clipboard`),
});
};
if (isLoading || !emailDomain) {
return (
<div>
<h2 className="text-4xl font-semibold">
<Trans>Email Domain</Trans>
</h2>
<p className="mt-4 text-muted-foreground">
<Trans>Loading...</Trans>
</p>
</div>
);
}
const pendingDuration =
emailDomain.status === EmailDomainStatus.PENDING
? DateTime.fromJSDate(new Date(emailDomain.createdAt)).toRelative()
: null;
return (
<div>
<div className="flex items-start justify-between">
<div className="flex items-center gap-x-4">
<h2 className="text-2xl font-semibold">{emailDomain.domain}</h2>
{match(emailDomain.status)
.with(EmailDomainStatus.ACTIVE, () => (
<Badge>
<CheckCircle2Icon className="mr-2 h-4 w-4 text-green-500 dark:text-green-300" />
<Trans>Active</Trans>
</Badge>
))
.with(EmailDomainStatus.PENDING, () => (
<Badge variant="warning">
<ClockIcon className="mr-2 h-4 w-4 text-yellow-500 dark:text-yellow-200" />
<Trans>Pending</Trans>
</Badge>
))
.exhaustive()}
</div>
</div>
<div className="mt-4 text-sm text-muted-foreground">
<div>
<Trans>ID</Trans>: {emailDomain.id}
</div>
<div>
<Trans>Organisation</Trans>:{' '}
<Link
to={`/admin/organisations/${emailDomain.organisation.id}`}
className="hover:underline"
>
{emailDomain.organisation.name}
</Link>
</div>
<div>
<Trans>Selector</Trans>: {emailDomain.selector}
</div>
<div>
<Trans>Created</Trans>: {i18n.date(emailDomain.createdAt, DateTime.DATETIME_MED)}
</div>
<div>
<Trans>Last Verified</Trans>:{' '}
{emailDomain.lastVerifiedAt
? i18n.date(emailDomain.lastVerifiedAt, DateTime.DATETIME_MED)
: '-'}
</div>
{pendingDuration && (
<div className="mt-1 text-yellow-600 dark:text-yellow-400">
<Trans>Pending since</Trans>: {pendingDuration}
</div>
)}
</div>
<hr className="my-4" />
<h3 className="text-lg font-semibold">
<Trans>Admin Actions</Trans>
</h3>
<div className="mt-2 flex gap-x-4">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" loading={isReregistering}>
<RotateCcwIcon className="mr-2 h-4 w-4" />
<Trans>Re-register</Trans>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<Trans>Re-register Email Domain</Trans>
</AlertDialogTitle>
<AlertDialogDescription>
<Trans>
This will delete the existing SES identity for{' '}
<strong>{emailDomain.domain}</strong> and recreate it using the same DKIM keys.
The user will not need to update their DNS records. The domain status will be
reset to Pending.
</Trans>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
<Trans>Cancel</Trans>
</AlertDialogCancel>
<AlertDialogAction
onClick={() => reregisterDomain({ emailDomainId: emailDomain.id })}
>
<Trans>Re-register</Trans>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
<hr className="my-4" />
<h3 className="text-lg font-semibold">
<Trans>DNS Records</Trans>
</h3>
<div className="mt-4 space-y-4">
{dnsRecords.map((record, index) => (
<div key={index} className="rounded-lg border p-4">
<div className="flex items-center justify-between">
<div className="text-sm font-medium">
{record.type} <Trans>Record</Trans>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => void onCopyToClipboard(record.value)}
>
<CopyIcon className="mr-2 h-4 w-4" />
<Trans>Copy Value</Trans>
</Button>
</div>
<div className="mt-2 space-y-1 text-sm">
<div>
<span className="text-muted-foreground">
<Trans>Name</Trans>:{' '}
</span>
<code className="rounded bg-muted px-1 py-0.5">{record.name}</code>
</div>
<div>
<span className="text-muted-foreground">
<Trans>Value</Trans>:{' '}
</span>
<code className="block break-all rounded bg-muted px-1 py-0.5">{record.value}</code>
</div>
</div>
</div>
))}
</div>
<hr className="my-4" />
<h3 className="text-lg font-semibold">
<Trans>Emails</Trans> ({emailDomain.emails.length})
</h3>
<div className="mt-4">
{emailDomain.emails.length > 0 ? (
<DataTable
columns={emailColumns}
data={emailDomain.emails}
perPage={emailDomain.emails.length}
currentPage={1}
totalPages={1}
onPaginationChange={() => {}}
/>
) : (
<p className="text-sm text-muted-foreground">
<Trans>No emails configured for this domain.</Trans>
</p>
)}
</div>
</div>
);
}
@@ -0,0 +1,207 @@
import { useMemo, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { EmailDomainStatus } from '@prisma/client';
import { CheckCircle2Icon, ClockIcon, Loader } from 'lucide-react';
import { Link, useSearchParams } from 'react-router';
import { match } from 'ts-pattern';
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { trpc } from '@documenso/trpc/react';
import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Input } from '@documenso/ui/primitives/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
export default function AdminEmailDomainsPage() {
const { _, i18n } = useLingui();
const [searchParams] = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const [term, setTerm] = useState(() => searchParams?.get?.('term') ?? '');
const debouncedTerm = useDebouncedValue(term, 500);
const page = searchParams?.get?.('page') ? Number(searchParams.get('page')) : undefined;
const perPage = searchParams?.get?.('perPage') ? Number(searchParams.get('perPage')) : undefined;
const statusParam = searchParams?.get?.('status') ?? 'ALL';
const statusFilter =
statusParam === 'PENDING' || statusParam === 'ACTIVE' ? statusParam : undefined;
const { data: findEmailDomainsData, isPending: isFindEmailDomainsLoading } =
trpc.admin.emailDomain.find.useQuery(
{
query: debouncedTerm,
page: page || 1,
perPage: perPage || 20,
status: statusFilter,
},
{
placeholderData: (previousData) => previousData,
},
);
const results = findEmailDomainsData ?? {
data: [],
perPage: 20,
currentPage: 1,
totalPages: 1,
};
const columns = useMemo(() => {
return [
{
header: _(msg`Domain`),
accessorKey: 'domain',
cell: ({ row }) => (
<Link
to={`/admin/email-domains/${row.original.id}`}
className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[15rem]"
>
{row.original.domain}
</Link>
),
},
{
header: _(msg`Organisation`),
accessorKey: 'organisation',
cell: ({ row }) => (
<Link
to={`/admin/organisations/${row.original.organisation.id}`}
className="hover:underline"
>
{row.original.organisation.name}
</Link>
),
},
{
header: _(msg`Status`),
accessorKey: 'status',
cell: ({ row }) =>
match(row.original.status)
.with(EmailDomainStatus.ACTIVE, () => (
<Badge>
<CheckCircle2Icon className="mr-2 h-4 w-4 text-green-500 dark:text-green-300" />
<Trans>Active</Trans>
</Badge>
))
.with(EmailDomainStatus.PENDING, () => (
<Badge variant="warning">
<ClockIcon className="mr-2 h-4 w-4 text-yellow-500 dark:text-yellow-200" />
<Trans>Pending</Trans>
</Badge>
))
.exhaustive(),
},
{
header: _(msg`Emails`),
accessorKey: '_count',
cell: ({ row }) => row.original._count.emails,
},
{
header: _(msg`Created`),
accessorKey: 'createdAt',
cell: ({ row }) => i18n.date(row.original.createdAt),
},
{
header: _(msg`Last Verified`),
accessorKey: 'lastVerifiedAt',
cell: ({ row }) =>
row.original.lastVerifiedAt ? i18n.date(row.original.lastVerifiedAt) : '-',
},
{
header: _(msg`Actions`),
cell: ({ row }) => (
<Button asChild variant="outline" size="sm">
<Link to={`/admin/email-domains/${row.original.id}`}>
<Trans>View</Trans>
</Link>
</Button>
),
},
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
}, []);
const onPaginationChange = (newPage: number, newPerPage: number) => {
updateSearchParams({
page: newPage,
perPage: newPerPage,
});
};
const onStatusChange = (value: string) => {
updateSearchParams({
status: value === 'ALL' ? undefined : value,
page: 1,
});
};
return (
<div>
<h2 className="text-4xl font-semibold">
<Trans>Email Domains</Trans>
</h2>
<div className="mt-8">
<div className="flex flex-col gap-4 sm:flex-row">
<Input
className="flex-1"
type="search"
placeholder={_(msg`Search by domain or organisation name`)}
value={term}
onChange={(e) => setTerm(e.target.value)}
/>
<Select value={statusParam} onValueChange={onStatusChange}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder={_(msg`Filter by status`)} />
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">
<Trans>All Statuses</Trans>
</SelectItem>
<SelectItem value="PENDING">
<Trans>Pending</Trans>
</SelectItem>
<SelectItem value="ACTIVE">
<Trans>Active</Trans>
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="relative mt-4">
<DataTable
columns={columns}
data={results.data}
perPage={results.perPage ?? 20}
currentPage={results.currentPage ?? 1}
totalPages={results.totalPages ?? 1}
onPaginationChange={onPaginationChange}
>
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
{isFindEmailDomainsLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
<Loader className="h-8 w-8 animate-spin text-gray-500" />
</div>
)}
</div>
</div>
</div>
);
}
@@ -9,6 +9,7 @@ import { match } from 'ts-pattern';
import { EnvelopeRenderProvider } from '@documenso/lib/client-only/providers/envelope-render-provider';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { PDF_VIEWER_ERROR_MESSAGES } from '@documenso/lib/constants/pdf-viewer-i18n';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
@@ -16,12 +17,12 @@ import {
DocumentReadOnlyFields,
mapFieldsWithRecipients,
} from '@documenso/ui/components/document/document-read-only-fields';
import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy';
import { EnvelopePdfViewer } from '@documenso/ui/components/pdf-viewer/envelope-pdf-viewer';
import { PDFViewer } from '@documenso/ui/components/pdf-viewer/pdf-viewer';
import { cn } from '@documenso/ui/lib/utils';
import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { Spinner } from '@documenso/ui/primitives/spinner';
import { DocumentPageViewButton } from '~/components/general/document/document-page-view-button';
@@ -154,7 +155,9 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
{envelope.internalVersion === 2 ? (
<div className="relative col-span-12 lg:col-span-6 xl:col-span-7">
<EnvelopeRenderProvider
version="current"
envelope={envelope}
envelopeItems={envelope.envelopeItems}
token={undefined}
fields={envelope.fields}
recipients={envelope.recipients}
@@ -169,9 +172,10 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
<Card className="rounded-xl before:rounded-xl" gradient>
<CardContent className="p-2">
<PDFViewerKonvaLazy
renderer="preview"
<EnvelopePdfViewer
customPageRenderer={EnvelopeGenericPageRenderer}
scrollParentRef="window"
errorMessage={PDF_VIEWER_ERROR_MESSAGES.preview}
/>
</CardContent>
</Card>
@@ -193,11 +197,12 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
/>
)}
<PDFViewerLazy
<PDFViewer
envelopeItem={envelope.envelopeItems[0]}
token={undefined}
key={envelope.envelopeItems[0].id}
version="signed"
version="current"
scrollParentRef="window"
/>
</CardContent>
</Card>
@@ -58,7 +58,7 @@ export default function EnvelopeEditorPage({ params }: Route.ComponentProps) {
if (envelope && (envelope.teamId !== team.id || envelope.internalVersion !== 2)) {
return (
<div className="text-foreground flex h-screen w-screen flex-col items-center justify-center gap-2">
<div className="flex h-screen w-screen flex-col items-center justify-center gap-2 text-foreground">
<Spinner />
<Trans>Redirecting</Trans>
</div>
@@ -67,7 +67,7 @@ export default function EnvelopeEditorPage({ params }: Route.ComponentProps) {
if (isLoadingEnvelope) {
return (
<div className="text-foreground flex h-screen w-screen flex-col items-center justify-center gap-2">
<div className="flex h-screen w-screen flex-col items-center justify-center gap-2 text-foreground">
<Spinner />
<Trans>Loading</Trans>
</div>
@@ -99,7 +99,9 @@ export default function EnvelopeEditorPage({ params }: Route.ComponentProps) {
return (
<EnvelopeEditorProvider initialEnvelope={envelope}>
<EnvelopeRenderProvider
version="current"
envelope={envelope}
envelopeItems={envelope.envelopeItems}
token={undefined}
fields={envelope.fields}
recipients={envelope.recipients}
@@ -78,7 +78,7 @@ export default function DocumentsFoldersPage() {
</div>
<div className="relative w-full max-w-md py-6">
<SearchIcon className="text-muted-foreground absolute left-2 top-9 h-4 w-4" />
<SearchIcon className="absolute left-2 top-9 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t`Search folders...`}
value={searchTerm}
@@ -93,7 +93,7 @@ export default function DocumentsFoldersPage() {
{isFoldersLoading ? (
<div className="mt-6 flex justify-center">
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : (
<>
@@ -128,7 +128,7 @@ export default function DocumentsFoldersPage() {
<div>
{searchTerm && foldersData?.folders.filter(isFolderMatchingSearch).length === 0 && (
<div className="text-muted-foreground mt-6 text-center">
<div className="mt-6 text-center text-muted-foreground">
<Trans>No folders found matching "{searchTerm}"</Trans>
</div>
)}
@@ -143,7 +143,7 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
<TooltipTrigger asChild>
<div
className={cn(
'text-muted-foreground/50 flex flex-row items-center justify-center space-x-2 text-xs',
'flex flex-row items-center justify-center space-x-2 text-xs text-muted-foreground/50',
{
'[&>*:first-child]:text-muted-foreground': !isPublicProfileVisible,
'[&>*:last-child]:text-muted-foreground': isPublicProfileVisible,
@@ -164,7 +164,7 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
</div>
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-[40ch] space-y-2 py-2">
<TooltipContent className="max-w-[40ch] space-y-2 py-2 text-muted-foreground">
{isPublicProfileVisible ? (
<>
<p>
@@ -8,15 +8,16 @@ import { Link, useNavigate } from 'react-router';
import { EnvelopeRenderProvider } from '@documenso/lib/client-only/providers/envelope-render-provider';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { PDF_VIEWER_ERROR_MESSAGES } from '@documenso/lib/constants/pdf-viewer-i18n';
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy';
import { EnvelopePdfViewer } from '@documenso/ui/components/pdf-viewer/envelope-pdf-viewer';
import { PDFViewer } from '@documenso/ui/components/pdf-viewer/pdf-viewer';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
import { Spinner } from '@documenso/ui/primitives/spinner';
import { TemplateBulkSendDialog } from '~/components/dialogs/template-bulk-send-dialog';
@@ -173,7 +174,9 @@ export default function TemplatePage({ params }: Route.ComponentProps) {
{envelope.internalVersion === 2 ? (
<div className="relative col-span-12 lg:col-span-6 xl:col-span-7">
<EnvelopeRenderProvider
version="current"
envelope={envelope}
envelopeItems={envelope.envelopeItems}
token={undefined}
fields={envelope.fields}
recipients={envelope.recipients}
@@ -187,9 +190,10 @@ export default function TemplatePage({ params }: Route.ComponentProps) {
<Card className="rounded-xl before:rounded-xl" gradient>
<CardContent className="p-2">
<PDFViewerKonvaLazy
renderer="preview"
<EnvelopePdfViewer
customPageRenderer={EnvelopeGenericPageRenderer}
scrollParentRef="window"
errorMessage={PDF_VIEWER_ERROR_MESSAGES.preview}
/>
</CardContent>
</Card>
@@ -210,11 +214,12 @@ export default function TemplatePage({ params }: Route.ComponentProps) {
documentMeta={mockedDocumentMeta}
/>
<PDFViewerLazy
<PDFViewer
envelopeItem={envelope.envelopeItems[0]}
token={undefined}
version="signed"
version="current"
key={envelope.envelopeItems[0].id}
scrollParentRef="window"
/>
</CardContent>
</Card>
@@ -78,7 +78,7 @@ export default function TemplatesFoldersPage() {
</div>
<div className="relative w-full max-w-md py-6">
<SearchIcon className="text-muted-foreground absolute left-2 top-9 h-4 w-4" />
<SearchIcon className="absolute left-2 top-9 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t`Search folders...`}
value={searchTerm}
@@ -93,7 +93,7 @@ export default function TemplatesFoldersPage() {
{isFoldersLoading ? (
<div className="mt- flex justify-center">
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : (
<>
@@ -128,7 +128,7 @@ export default function TemplatesFoldersPage() {
<div>
{searchTerm && foldersData?.folders.filter(isFolderMatchingSearch).length === 0 && (
<div className="text-muted-foreground mt-6 text-center">
<div className="mt-6 text-center text-muted-foreground">
<Trans>No folders found matching "{searchTerm}"</Trans>
</div>
)}
+10 -10
View File
@@ -64,7 +64,7 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
return (
<div className="flex flex-col items-center justify-center py-4 sm:py-32">
<div className="flex flex-col items-center">
<Avatar className="dark:border-border h-24 w-24 border-2 border-solid">
<Avatar className="h-24 w-24 border-2 border-solid dark:border-border">
{publicProfile.avatarImageId && (
<AvatarImage src={formatAvatarUrl(publicProfile.avatarImageId)} />
)}
@@ -99,10 +99,10 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
/>
<div className="ml-2">
<p className="text-foreground text-base font-semibold">
<p className="text-base font-semibold text-foreground">
{BADGE_DATA[publicProfile.badge.type].name}
</p>
<p className="text-muted-foreground mt-0.5 text-sm">
<p className="mt-0.5 text-sm text-muted-foreground">
<Trans>
Since {DateTime.fromJSDate(publicProfile.badge.since).toFormat('LLL yy')}
</Trans>
@@ -113,7 +113,7 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
)}
</div>
<div className="text-muted-foreground mt-4 space-y-1">
<div className="mt-4 space-y-1 text-muted-foreground">
{(profile.bio ?? '').split('\n').map((line, index) => (
<p
key={index}
@@ -127,7 +127,7 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
{templates.length === 0 && (
<div className="mt-4 w-full max-w-xl border-t pt-4">
<p className="text-muted-foreground max-w-[60ch] whitespace-pre-wrap break-words text-center text-sm leading-relaxed">
<p className="max-w-[60ch] whitespace-pre-wrap break-words text-center text-sm leading-relaxed text-muted-foreground">
<Trans>
It looks like {publicProfile.name} hasn't added any documents to their profile yet.
</Trans>{' '}
@@ -167,24 +167,24 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
<TableBody>
{templates.map((template) => (
<TableRow key={template.id}>
<TableCell className="text-muted-foreground flex flex-col justify-between overflow-hidden text-sm sm:flex-row">
<TableCell className="flex flex-col justify-between overflow-hidden text-sm text-muted-foreground sm:flex-row">
<div className="flex flex-1 items-start justify-start gap-2">
<FileIcon
className="text-muted-foreground/40 h-8 w-8 flex-shrink-0"
className="h-8 w-8 flex-shrink-0 text-muted-foreground/40"
strokeWidth={1.5}
/>
<div className="flex flex-1 flex-col gap-4 overflow-hidden md:flex-row md:items-start md:justify-between">
<div>
<p className="text-foreground text-sm font-semibold leading-none">
<p className="text-sm font-semibold leading-none text-foreground">
{template.publicTitle}
</p>
<p className="text-muted-foreground mt-1 line-clamp-3 max-w-[70ch] whitespace-normal text-xs">
<p className="mt-1 line-clamp-3 max-w-[70ch] whitespace-normal text-xs text-muted-foreground">
{template.publicDescription}
</p>
</div>
<Button asChild className="w-20">
<Button asChild className="w-fit">
<Link to={formatDirectTemplatePath(template.directLink.token)}>
<Trans>Sign</Trans>
</Link>
@@ -198,7 +198,7 @@ const DirectSigningPageV1 = ({ data }: { data: Awaited<ReturnType<typeof handleV
{template.title}
</h1>
<div className="text-muted-foreground mb-8 mt-2.5 flex items-center gap-x-2">
<div className="mb-8 mt-2.5 flex items-center gap-x-2 text-muted-foreground">
<UsersIcon className="h-4 w-4" />
<p className="text-muted-foreground/80">
<Plural value={template.recipients.length} one="# recipient" other="# recipients" />
@@ -246,7 +246,12 @@ const DirectSigningPageV2 = ({ data }: { data: Awaited<ReturnType<typeof handleV
recipient={recipient}
user={user}
>
<EnvelopeRenderProvider envelope={envelope} token={recipient.token}>
<EnvelopeRenderProvider
version="current"
envelope={envelope}
envelopeItems={envelope.envelopeItems}
token={recipient.token}
>
<DocumentSigningPageViewV2 />
</EnvelopeRenderProvider>
</DocumentSigningAuthProvider>
@@ -504,7 +504,12 @@ const SigningPageV2 = ({ data }: { data: Awaited<ReturnType<typeof handleV2Loade
recipient={recipient}
user={user}
>
<EnvelopeRenderProvider envelope={envelope} token={recipient.token}>
<EnvelopeRenderProvider
version="current"
envelope={envelope}
envelopeItems={envelope.envelopeItems}
token={recipient.token}
>
<DocumentSigningPageViewV2 />
</EnvelopeRenderProvider>
</DocumentSigningAuthProvider>
@@ -320,7 +320,12 @@ const EmbedDirectTemplatePageV2 = ({
user={user}
isDirectTemplate={true}
>
<EnvelopeRenderProvider envelope={envelope} token={recipient.token}>
<EnvelopeRenderProvider
version="current"
envelope={envelope}
envelopeItems={envelope.envelopeItems}
token={recipient.token}
>
<EmbedSignDocumentV2ClientPage
hidePoweredBy={hidePoweredBy}
allowWhitelabelling={allowEmbedSigningWhitelabel}
@@ -405,7 +405,12 @@ const EmbedSignDocumentPageV2 = ({
recipient={recipient}
user={user}
>
<EnvelopeRenderProvider envelope={envelope} token={token}>
<EnvelopeRenderProvider
version="current"
envelope={envelope}
envelopeItems={envelope.envelopeItems}
token={token}
>
<EmbedSignDocumentV2ClientPage
hidePoweredBy={hidePoweredBy}
allowWhitelabelling={allowEmbedSigningWhitelabel}
@@ -283,7 +283,7 @@ export default function MultisignPage() {
</DocumentSigningProvider>
{!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">
<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>
@@ -300,7 +300,7 @@ export default function MultisignPage() {
<MultiSignDocumentList envelopes={envelopes} onDocumentSelect={onSelectDocument} />
{!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">
<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>
+12
View File
@@ -23,6 +23,10 @@ import {
ZGetPresignedPostUrlRequestSchema,
ZUploadPdfRequestSchema,
} from './files.types';
import getEnvelopeItemImageRoute from './routes/get-envelope-item-image';
import getEnvelopeItemImageByTokenRoute from './routes/get-envelope-item-image-by-token';
import getEnvelopeItemMetaRoute from './routes/get-envelope-item-meta';
import getEnvelopeItemMetaByTokenRoute from './routes/get-envelope-item-meta-by-token';
export const filesRoute = new Hono<HonoEnv>()
/**
@@ -319,3 +323,11 @@ export const filesRoute = new Hono<HonoEnv>()
});
},
);
// Envelope item meta routes for both tokens and auth based
filesRoute.route('/', getEnvelopeItemMetaRoute);
filesRoute.route('/', getEnvelopeItemMetaByTokenRoute);
// Image routes for both tokens and auth based
filesRoute.route('/', getEnvelopeItemImageRoute);
filesRoute.route('/', getEnvelopeItemImageByTokenRoute);
@@ -1,3 +1,4 @@
import { DocumentDataType } from '@prisma/client';
import { z } from 'zod';
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
@@ -72,3 +73,24 @@ export const ZGetEnvelopeItemFileTokenDownloadRequestParamsSchema = z.object({
export type TGetEnvelopeItemFileTokenDownloadRequestParams = z.infer<
typeof ZGetEnvelopeItemFileTokenDownloadRequestParamsSchema
>;
export const ZGetEnvelopeItemMetaSchema = z.object({
envelopeItemId: z.string(),
documentDataId: z.string(),
documentDataType: z.nativeEnum(DocumentDataType),
pages: z
.object({
originalWidth: z.number(),
originalHeight: z.number(),
scale: z.number(),
scaledWidth: z.number(),
scaledHeight: z.number(),
})
.array(),
});
export const ZGetEnvelopeItemsMetaResponseSchema = z.object({
envelopeItems: z.array(ZGetEnvelopeItemMetaSchema),
});
export type TGetEnvelopeItemsMetaResponse = z.infer<typeof ZGetEnvelopeItemsMetaResponseSchema>;
@@ -0,0 +1,64 @@
import { sValidator } from '@hono/standard-validator';
import { Hono } from 'hono';
import { z } from 'zod';
import { prisma } from '@documenso/prisma';
import type { HonoEnv } from '../../../router';
import { handleEnvelopeItemPageRequest } from './get-envelope-item-image';
const route = new Hono<HonoEnv>();
const ZGetEnvelopeItemPageTokenParamsSchema = z.object({
token: z.string().min(1),
envelopeId: z.string().min(1),
envelopeItemId: z.string().min(1),
documentDataId: z.string().min(1),
version: z.enum(['initial', 'current']),
pageIndex: z.coerce.number().int().min(0),
});
/**
* Returns a single PDF page as a JPEG image using a token.
*/
route.get(
'/token/:token/envelope/:envelopeId/envelopeItem/:envelopeItemId/dataId/:documentDataId/:version/:pageIndex/image.jpeg',
sValidator('param', ZGetEnvelopeItemPageTokenParamsSchema),
async (c) => {
const { token, envelopeId, envelopeItemId, documentDataId, version, pageIndex } =
c.req.valid('param');
// Validate envelope access.
const envelopeItem = await prisma.envelopeItem.findFirst({
where: {
id: envelopeItemId,
documentDataId,
envelope: {
id: envelopeId,
recipients: {
some: {
token,
},
},
},
},
include: {
documentData: true,
},
});
if (!envelopeItem) {
return c.json({ error: 'Not found' }, 404);
}
return await handleEnvelopeItemPageRequest({
c,
envelopeItem,
version,
pageIndex,
cacheStrategy: 'public',
});
},
);
export default route;
@@ -0,0 +1,180 @@
import { sValidator } from '@hono/standard-validator';
import type { DocumentData, EnvelopeItem } from '@prisma/client';
import { type Context, Hono } from 'hono';
import { z } from 'zod';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { PDF_IMAGE_RENDER_SCALE } from '@documenso/lib/constants/pdf-viewer';
import { pdfToImage } from '@documenso/lib/server-only/ai/pdf-to-images';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import type { DocumentDataVersion } from '@documenso/lib/types/document-data';
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
import { UNSAFE_getS3File } from '@documenso/lib/universal/upload/server-actions';
import { getEnvelopeItemPageImageS3Key } from '@documenso/lib/utils/envelope-images';
import { prisma } from '@documenso/prisma';
import type { HonoEnv } from '../../../router';
const route = new Hono<HonoEnv>();
const ZGetEnvelopeItemPageRequestParamsSchema = z.object({
envelopeId: z.string().min(1),
envelopeItemId: z.string().min(1),
documentDataId: z.string().min(1),
version: z.enum(['initial', 'current']),
pageIndex: z.coerce.number().int().min(0),
});
const ZGetEnvelopeItemPageRequestQuerySchema = z.object({
presignToken: z.string().optional(),
});
/**
* Returns a single PDF page as a JPEG image.
*/
route.get(
'/envelope/:envelopeId/envelopeItem/:envelopeItemId/dataId/:documentDataId/:version/:pageIndex/image.jpeg',
sValidator('param', ZGetEnvelopeItemPageRequestParamsSchema),
sValidator('query', ZGetEnvelopeItemPageRequestQuerySchema),
async (c) => {
const { envelopeId, envelopeItemId, documentDataId, version, pageIndex } = c.req.valid('param');
const { presignToken } = c.req.valid('query');
const session = await getOptionalSession(c);
let userId = session.user?.id;
// Check presignToken if provided
if (presignToken) {
const verifiedToken = await verifyEmbeddingPresignToken({
token: presignToken,
}).catch(() => undefined);
userId = verifiedToken?.userId;
}
if (!userId) {
return c.json({ error: 'Not found' }, 404);
}
const envelope = await prisma.envelope.findFirst({
where: { id: envelopeId },
include: {
envelopeItems: {
where: {
id: envelopeItemId,
documentDataId,
},
include: {
documentData: true,
},
},
},
});
if (!envelope) {
return c.json({ error: 'Not found' }, 404);
}
const [envelopeItem] = envelope.envelopeItems;
if (!envelopeItem?.documentData) {
return c.json({ error: 'Not found' }, 404);
}
// Check team access
const team = await getTeamById({
userId,
teamId: envelope.teamId,
}).catch(() => null);
if (!team) {
return c.json({ error: 'Not found' }, 404);
}
return await handleEnvelopeItemPageRequest({
c,
envelopeItem,
version,
pageIndex,
cacheStrategy: 'private',
});
},
);
type HandleEnvelopeItemPageRequestOptions = {
c: Context<HonoEnv>;
envelopeItem: EnvelopeItem & {
documentData: DocumentData;
};
pageIndex: number;
version: DocumentDataVersion;
/**
* The type of cache strategy to use.
*
* For access via tokens, we can use a public cache to allow the CDN to cache it.
*
* For access via session, we must use a private cache.
*/
cacheStrategy: 'private' | 'public';
};
export const handleEnvelopeItemPageRequest = async ({
c,
envelopeItem,
pageIndex,
version,
cacheStrategy,
}: HandleEnvelopeItemPageRequestOptions) => {
// Determine which PDF data to use based on version requested.
const documentDataToUse =
version === 'current' ? envelopeItem.documentData.data : envelopeItem.documentData.initialData;
// Return the image if it already exists in S3.
if (envelopeItem.documentData.type === 'S3_PATH') {
const s3Key = getEnvelopeItemPageImageS3Key(documentDataToUse, pageIndex);
const image = await UNSAFE_getS3File(s3Key).catch(() => null);
if (image) {
// Note: Only set these headers on success.
c.header('Content-Type', 'image/jpeg');
c.header('Cache-Control', `${cacheStrategy}, max-age=31536000, immutable`);
return c.body(image);
}
}
// Fetch PDF to render the page on the spot if it doesn't exist in S3.
const pdfBytes = await getFileServerSide({
type: envelopeItem.documentData.type,
data: documentDataToUse,
});
// Render page to image.
const { image } = await pdfToImage(pdfBytes, {
scale: PDF_IMAGE_RENDER_SCALE,
pageIndex,
}).catch((err) => {
console.error(err);
return {
image: null,
};
});
if (!image) {
return c.json({ error: 'Failed to render page to image' }, 500);
}
// Note: Only set these headers on success.
c.header('Content-Type', 'image/jpeg');
c.header('Cache-Control', `${cacheStrategy}, max-age=31536000, immutable`);
return c.body(image);
};
export default route;
@@ -0,0 +1,54 @@
import { sValidator } from '@hono/standard-validator';
import { Hono } from 'hono';
import { z } from 'zod';
import { prisma } from '@documenso/prisma';
import type { HonoEnv } from '../../../router';
import { handleEnvelopeItemsMetaRequest } from './get-envelope-item-meta';
const route = new Hono<HonoEnv>();
const ZGetEnvelopeMetaByTokenParamSchema = z.object({
token: z.string().min(1),
envelopeId: z.string().min(1),
});
/**
* Returns metadata for all envelope items including page counts and dimensions using a token.
*/
route.get(
'/token/:token/envelope/:envelopeId/meta',
sValidator('param', ZGetEnvelopeMetaByTokenParamSchema),
async (c) => {
const { token, envelopeId } = c.req.valid('param');
// Validate token belongs to envelope
const recipient = await prisma.recipient.findFirst({
where: {
token,
envelopeId,
},
select: {
envelope: {
include: {
envelopeItems: {
include: { documentData: true },
},
},
},
},
});
if (!recipient) {
return c.json({ error: 'Not found' }, 404);
}
return await handleEnvelopeItemsMetaRequest({
c,
envelopeItems: recipient.envelope.envelopeItems,
});
},
);
export default route;
@@ -0,0 +1,140 @@
import { sValidator } from '@hono/standard-validator';
import type { DocumentData, EnvelopeItem } from '@prisma/client';
import { type Context, Hono } from 'hono';
import { z } from 'zod';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import type { TDocumentDataMeta } from '@documenso/lib/types/document-data';
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
import { extractAndStorePdfImages } from '@documenso/lib/universal/upload/put-file.server';
import { prisma } from '@documenso/prisma';
import type { HonoEnv } from '../../../router';
import type { TGetEnvelopeItemsMetaResponse } from '../files.types';
const route = new Hono<HonoEnv>();
const ZGetEnvelopeMetaParamsSchema = z.object({
envelopeId: z.string().min(1),
});
const ZGetEnvelopeMetaQuerySchema = z.object({
presignToken: z.string().optional(),
});
/**
* Returns metadata for all envelope items including page counts and dimensions.
*/
route.get(
'/envelope/:envelopeId/meta',
sValidator('param', ZGetEnvelopeMetaParamsSchema),
sValidator('query', ZGetEnvelopeMetaQuerySchema),
async (c) => {
const { envelopeId } = c.req.valid('param');
const { presignToken } = c.req.valid('query');
const session = await getOptionalSession(c);
let userId = session.user?.id;
// Check presignToken if provided
if (presignToken) {
const verifiedToken = await verifyEmbeddingPresignToken({
token: presignToken,
}).catch(() => undefined);
userId = verifiedToken?.userId;
}
if (!userId) {
return c.json({ error: 'Not found' }, 404);
}
// Note: Access is verified in the getTeamById call after this.
const envelope = await prisma.envelope.findFirst({
where: {
id: envelopeId,
},
include: {
envelopeItems: {
include: { documentData: true },
},
},
});
if (!envelope) {
return c.json({ error: 'Not found' }, 404);
}
// Check access to envelope.
const team = await getTeamById({
userId,
teamId: envelope.teamId,
}).catch(() => null);
if (!team) {
return c.json({ error: 'Not found' }, 404);
}
return await handleEnvelopeItemsMetaRequest({
c,
envelopeItems: envelope.envelopeItems,
});
},
);
type HandleEnvelopeItemsMetaRequestOptions = {
c: Context<HonoEnv>;
envelopeItems: (EnvelopeItem & {
documentData: DocumentData;
})[];
};
export const handleEnvelopeItemsMetaRequest = async ({
c,
envelopeItems,
}: HandleEnvelopeItemsMetaRequestOptions) => {
const response = await Promise.all(
envelopeItems.map(async (item) => {
let pageMetadata = item.documentData.metadata;
// Runtime backfill if pageMetadata is missing.
if (!pageMetadata) {
const pdfBytes = await getFileServerSide({
type: item.documentData.type,
data: item.documentData.data,
});
const pdfPageMetadata: TDocumentDataMeta['pages'] = await extractAndStorePdfImages(
new Uint8Array(pdfBytes).buffer,
item.documentData.id,
);
pageMetadata = {
pages: pdfPageMetadata,
};
}
const pages = pageMetadata.pages ?? [];
return {
envelopeItemId: item.id,
documentDataId: item.documentData.id,
documentDataType: item.documentData.type,
pages: pages.map((page) => ({
originalWidth: page.originalWidth,
originalHeight: page.originalHeight,
scale: page.scale,
scaledWidth: page.scaledWidth,
scaledHeight: page.scaledHeight,
})),
};
}),
);
return c.json({ envelopeItems: response } satisfies TGetEnvelopeItemsMetaResponse);
};
export default route;
+24 -88
View File
@@ -15,9 +15,10 @@
"dependencies": {
"@ai-sdk/google-vertex": "3.0.81",
"@documenso/prisma": "*",
"@libpdf/core": "^0.2.5",
"@libpdf/core": "^0.2.9",
"@lingui/conf": "^5.6.0",
"@lingui/core": "^5.6.0",
"@prisma/extension-read-replicas": "^0.4.1",
"ai": "^5.0.104",
"cron-parser": "^5.5.0",
"luxon": "^3.7.2",
@@ -4167,14 +4168,15 @@
"license": "MIT"
},
"node_modules/@libpdf/core": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@libpdf/core/-/core-0.2.5.tgz",
"integrity": "sha512-+oTpNRkEdL1kVmeJr6qz2wf0yqJx5FmUVN2u0kDuX81wvxyzYOlMjmFD8qbbJqyYiNZp0J7IAcW6VsZr+MW1Uw==",
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@libpdf/core/-/core-0.2.9.tgz",
"integrity": "sha512-BpZvJr9mf2ALQwv7jPMNLmOjiYxytF8+2UO4mt/S3cg5Brw62/UEpBs8IYa4zXsAR/yQW4vVkkfE4OnOYd6Ktw==",
"license": "MIT",
"dependencies": {
"@noble/ciphers": "^2.1.1",
"@noble/hashes": "^2.0.1",
"@scure/base": "^2.0.0",
"lru-cache": "^11.2.6",
"pako": "^2.1.0",
"pkijs": "^3.3.3"
},
@@ -4227,6 +4229,15 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@libpdf/core/node_modules/lru-cache": {
"version": "11.2.6",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
"integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@libpdf/core/node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
@@ -12183,6 +12194,15 @@
"integrity": "sha512-gV7uOBQfAFlWDvPJdQxMT1aSRur3a0EkU/6cfbAC5isV67tKDWUrPauyaHNpB+wN1ebM4A9jn/f4gH+3iHSYSQ==",
"license": "Apache-2.0"
},
"node_modules/@prisma/extension-read-replicas": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@prisma/extension-read-replicas/-/extension-read-replicas-0.4.1.tgz",
"integrity": "sha512-mCMDloqUKUwx2o5uedTs1FHX3Nxdt1GdRMoeyp1JggjiwOALmIYWhxfIN08M2BZ0w8SKwvJqicJZMjkQYkkijw==",
"license": "Apache-2.0",
"peerDependencies": {
"@prisma/client": "^6.5.0"
}
},
"node_modules/@prisma/fetch-engine": {
"version": "6.19.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.0.tgz",
@@ -27368,24 +27388,6 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/make-cancellable-promise": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-2.0.0.tgz",
"integrity": "sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==",
"license": "MIT",
"funding": {
"url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1"
}
},
"node_modules/make-event-props": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-2.0.0.tgz",
"integrity": "sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw==",
"license": "MIT",
"funding": {
"url": "https://github.com/wojtekmaj/make-event-props?sponsor=1"
}
},
"node_modules/map-stream": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
@@ -27832,23 +27834,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/merge-refs": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-2.0.0.tgz",
"integrity": "sha512-3+B21mYK2IqUWnd2EivABLT7ueDhb0b8/dGK8LoFQPrU61YITeCMn14F7y7qZafWNZhUEKb24cJdiT5Wxs3prg==",
"license": "MIT",
"funding": {
"url": "https://github.com/wojtekmaj/merge-refs?sponsor=1"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -31878,44 +31863,6 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-pdf": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-10.3.0.tgz",
"integrity": "sha512-2LQzC9IgNVAX8gM+6F+1t/70a9/5RWThYxc+CWAmT2LW/BRmnj+35x1os5j/nR2oldyf8L+hCAMBmVKU8wrYFA==",
"license": "MIT",
"dependencies": {
"clsx": "^2.0.0",
"dequal": "^2.0.3",
"make-cancellable-promise": "^2.0.0",
"make-event-props": "^2.0.0",
"merge-refs": "^2.0.0",
"pdfjs-dist": "5.4.296",
"tiny-invariant": "^1.0.0",
"warning": "^4.0.0"
},
"funding": {
"url": "https://github.com/wojtekmaj/react-pdf?sponsor=1"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-pdf/node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/react-promise-suspense": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz",
@@ -36485,15 +36432,6 @@
"node": ">=20.0.0"
}
},
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
@@ -37437,7 +37375,6 @@
"posthog-js": "^1.297.2",
"posthog-node": "4.18.0",
"react": "^18",
"react-pdf": "^10.3.0",
"remeda": "^2.32.0",
"sharp": "0.34.5",
"skia-canvas": "^3.0.8",
@@ -37595,7 +37532,6 @@
"react-day-picker": "^8.10.1",
"react-dom": "^18",
"react-hook-form": "^7.66.1",
"react-pdf": "^10.3.0",
"react-rnd": "^10.5.2",
"remeda": "^2.32.0",
"tailwind-merge": "^1.14.0",
+2 -1
View File
@@ -86,9 +86,10 @@
"dependencies": {
"@ai-sdk/google-vertex": "3.0.81",
"@documenso/prisma": "*",
"@libpdf/core": "^0.2.5",
"@libpdf/core": "^0.2.9",
"@lingui/conf": "^5.6.0",
"@lingui/core": "^5.6.0",
"@prisma/extension-read-replicas": "^0.4.1",
"ai": "^5.0.104",
"cron-parser": "^5.5.0",
"luxon": "^3.7.2",
@@ -1,6 +1,7 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { prisma } from '@documenso/prisma';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
@@ -46,7 +47,7 @@ test.describe('AutoSave Fields Step', () => {
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -54,7 +55,7 @@ test.describe('AutoSave Fields Step', () => {
});
await page.getByRole('button', { name: 'Text' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -74,7 +75,7 @@ test.describe('AutoSave Fields Step', () => {
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 500,
@@ -100,7 +101,7 @@ test.describe('AutoSave Fields Step', () => {
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -108,7 +109,7 @@ test.describe('AutoSave Fields Step', () => {
});
await page.getByRole('button', { name: 'Text' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -128,7 +129,7 @@ test.describe('AutoSave Fields Step', () => {
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 500,
@@ -162,7 +163,7 @@ test.describe('AutoSave Fields Step', () => {
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -170,7 +171,7 @@ test.describe('AutoSave Fields Step', () => {
});
await page.getByRole('button', { name: 'Text' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -190,7 +191,7 @@ test.describe('AutoSave Fields Step', () => {
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 500,
@@ -224,7 +225,7 @@ test.describe('AutoSave Fields Step', () => {
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -232,7 +233,7 @@ test.describe('AutoSave Fields Step', () => {
});
await page.getByRole('button', { name: 'Text' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -2,6 +2,7 @@ import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { EnvelopeType } from '@prisma/client';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
@@ -28,7 +29,7 @@ export const setupDocumentAndNavigateToSubjectStep = async (page: Page) => {
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -108,7 +109,9 @@ test.describe('AutoSave Subject Step', () => {
// 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 the document is completed', { exact: true })
.click();
await page.getByText('Email recipients when a pending document is deleted').click();
await triggerAutosave(page);
@@ -139,16 +142,20 @@ test.describe('AutoSave Subject Step', () => {
).toBeChecked({
checked: emailSettings?.documentCompleted,
});
await expect(page.getByText('Email recipients when a pending document is deleted')).toBeChecked({
await expect(
page.getByText('Email recipients when a pending document is deleted'),
).toBeChecked({
checked: emailSettings?.documentDeleted,
});
await expect(page.getByText('Email recipients with a signing request')).toBeChecked({
checked: emailSettings?.recipientSigningRequest,
});
await expect(page.getByText('Email the signer if the document is still pending')).toBeChecked({
checked: emailSettings?.documentPending,
});
await expect(page.getByText('Email the signer if the document is still pending')).toBeChecked(
{
checked: emailSettings?.documentPending,
},
);
await expect(page.getByText('Email the owner when the document is completed')).toBeChecked({
checked: emailSettings?.ownerDocumentCompleted,
});
@@ -167,7 +174,9 @@ test.describe('AutoSave Subject Step', () => {
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 the document is completed', { exact: true })
.click();
await page.getByText('Email recipients when a pending document is deleted').click();
await triggerAutosave(page);
@@ -207,16 +216,20 @@ test.describe('AutoSave Subject Step', () => {
).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentCompleted,
});
await expect(page.getByText('Email recipients when a pending document is deleted')).toBeChecked({
await expect(
page.getByText('Email recipients when a pending document is deleted'),
).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentDeleted,
});
await expect(page.getByText('Email recipients with a signing request')).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.recipientSigningRequest,
});
await expect(page.getByText('Email the signer if the document is still pending')).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentPending,
});
await expect(page.getByText('Email the signer if the document is still pending')).toBeChecked(
{
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentPending,
},
);
await expect(page.getByText('Email the owner when the document is completed')).toBeChecked({
checked: retrievedDocumentData.documentMeta?.emailSettings?.ownerDocumentCompleted,
});
@@ -1,5 +1,6 @@
import { expect, test } from '@playwright/test';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
@@ -33,14 +34,14 @@ test('[DOCUMENT_FLOW]: Simple duplicate recipients test', async ({ page }) => {
// Step 3: Add fields
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 100, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 100, y: 100 } });
await page.getByRole('combobox').first().click();
// Switch to second duplicate and add field
await page.getByText('Duplicate 2 (duplicate@example.com)').first().click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 200, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 200, y: 100 } });
// Continue to send
await page.getByRole('button', { name: 'Continue' }).click();
@@ -44,21 +44,21 @@ const completeDocumentFlowWithDuplicateRecipients = async (options: {
// Step 3: Add fields for each recipient
// Add signature field for first duplicate recipient
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 100, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 100, y: 100 } });
await page.getByText('Duplicate Recipient 1 (duplicate@example.com)').click();
// Switch to second duplicate recipient and add their field
await page.getByText('Duplicate Recipient 2 (duplicate@example.com)').click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 200, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 200, y: 100 } });
await page.getByText('Duplicate Recipient 2 (duplicate@example.com)').click();
// Switch to unique recipient and add their field
await page.getByText('Unique Recipient (unique@example.com)').click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 300, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 300, y: 100 } });
// Continue to subject
await page.getByRole('button', { name: 'Continue' }).click();
@@ -122,7 +122,7 @@ test.describe('[DOCUMENT_FLOW]: Duplicate Recipients', () => {
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 100, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 100, y: 100 } });
// Save the document by going to subject
await page.getByRole('button', { name: 'Continue' }).click();
@@ -149,7 +149,7 @@ test.describe('[DOCUMENT_FLOW]: Duplicate Recipients', () => {
await page.getByText('Test Recipient Duplicate (test@example.com)').first().click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 200, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 200, y: 100 } });
// Complete the flow
await page.getByRole('button', { name: 'Continue' }).click();
@@ -270,24 +270,24 @@ test.describe('[DOCUMENT_FLOW]: Duplicate Recipients', () => {
// Add signature for first recipient
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 100, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 100, y: 100 } });
// Add name field for second recipient
await page.getByRole('combobox').first().click();
await page.getByText('Approver Role (signer@example.com)').first().click();
await page.getByRole('button', { name: 'Name' }).click();
await page.locator('canvas').click({ position: { x: 200, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 200, y: 100 } });
// Add date field for second recipient
await page.getByRole('button', { name: 'Date' }).click();
await page.locator('canvas').click({ position: { x: 200, y: 150 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 200, y: 150 } });
// If second recipient is still a SIGNER (role change wasn't available),
// add a signature field for them to pass validation
if (!secondRecipientIsApprover) {
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 200, y: 200 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 200, y: 200 } });
}
// Complete the document
@@ -349,7 +349,7 @@ test.describe('[DOCUMENT_FLOW]: Duplicate Recipients', () => {
// Add another field to the second duplicate
await page.getByRole('button', { name: 'Name' }).click();
await page.locator('canvas').click({ position: { x: 250, y: 150 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 250, y: 150 } });
// Save changes
await page.getByRole('button', { name: 'Continue' }).click();
@@ -9,6 +9,7 @@ import {
import { DateTime } from 'luxon';
import path from 'node:path';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { prisma } from '@documenso/prisma';
import {
seedBlankDocument,
@@ -92,7 +93,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document', async ({ page }) =>
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -100,7 +101,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document', async ({ page }) =>
});
await page.getByRole('button', { name: 'Email' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -158,7 +159,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -166,7 +167,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
});
await page.getByRole('button', { name: 'Email' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -177,7 +178,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
await page.getByText('User 2 (user2@example.com)').click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 500,
y: 100,
@@ -185,7 +186,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
});
await page.getByRole('button', { name: 'Email' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 500,
y: 200,
@@ -256,7 +257,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
await page.getByRole('option', { name: 'User 1 (user1@example.com)' }).click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -264,7 +265,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
});
await page.getByRole('button', { name: 'Email' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -275,7 +276,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
await page.getByRole('option', { name: 'User 3 (user3@example.com)' }).click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 500,
y: 100,
@@ -283,7 +284,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
});
await page.getByRole('button', { name: 'Email' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 500,
y: 200,
@@ -576,7 +577,7 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip
}
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100 * i,
@@ -1,13 +1,12 @@
import { createCanvas } from '@napi-rs/canvas';
import type { TestInfo } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { DocumentStatus, EnvelopeType } from '@prisma/client';
import fs from 'node:fs';
import path from 'node:path';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
import pixelMatch from 'pixelmatch';
import { PNG } from 'pngjs';
import { pdfToImages } from '@documenso/lib/server-only/ai/pdf-to-images';
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
import { prisma } from '@documenso/prisma';
import { seedAlignmentTestDocument } from '@documenso/prisma/seed/initial-seed';
@@ -394,39 +393,14 @@ test.skip('download envelope images', async ({ page, request }) => {
});
async function renderPdfToImage(pdfBytes: Uint8Array) {
const loadingTask = pdfjsLib.getDocument({ data: pdfBytes });
const pdf = await loadingTask.promise;
// Increase for higher resolution
const scale = 4;
return await Promise.all(
Array.from({ length: pdf.numPages }, async (_, index) => {
const page = await pdf.getPage(index + 1);
const viewport = page.getViewport({ scale });
const canvas = createCanvas(viewport.width, viewport.height);
const canvasContext = canvas.getContext('2d');
canvasContext.imageSmoothingEnabled = false;
await page.render({
// @ts-expect-error @napi-rs/canvas satisfies runtime requirements for pdfjs
canvas,
// @ts-expect-error @napi-rs/canvas satisfies runtime requirements for pdfjs
canvasContext,
viewport,
}).promise;
return {
image: await canvas.encode('png'),
// Rounded down because the certificate page somehow gives dimensions with decimals
width: Math.floor(viewport.width),
height: Math.floor(viewport.height),
};
}),
);
return (await pdfToImages(pdfBytes, { scale, imageFormat: 'png' })).map((image) => ({
image: image.image,
width: Math.floor(image.scaledWidth),
height: Math.floor(image.scaledHeight),
}));
}
type CompareSignedPdfWithImagesOptions = {
@@ -0,0 +1,270 @@
import { expect, test } from '@playwright/test';
import { FieldType } from '@prisma/client';
import path from 'node:path';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/create-embedding-presign-token';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import { prefixedId } from '@documenso/lib/universal/id';
import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
import { prisma } from '@documenso/prisma';
import {
seedBlankDocument,
seedPendingDocumentWithFullFields,
} from '@documenso/prisma/seed/documents';
import { seedBlankTemplate, seedDirectTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import { apiSignin } from '../fixtures/authentication';
const PDF_PAGE_SELECTOR = 'img[data-page-number]';
async function addSecondEnvelopeItem(envelopeId: string) {
const firstItem = await prisma.envelopeItem.findFirstOrThrow({
where: { envelopeId },
orderBy: { order: 'asc' },
include: { documentData: true },
});
const newDocumentData = await prisma.documentData.create({
data: {
type: firstItem.documentData.type,
data: firstItem.documentData.data,
initialData: firstItem.documentData.initialData,
},
});
await prisma.envelopeItem.create({
data: {
id: prefixedId('envelope_item'),
title: `${firstItem.title} - Page 2`,
documentDataId: newDocumentData.id,
order: 2,
envelopeId,
},
});
}
test.describe('PDF Viewer Rendering', () => {
test.describe('Authenticated Pages', () => {
test('should render PDF on all authenticated pages (V1 and V2)', async ({ page }) => {
const { user, team } = await seedUser();
const documentV1 = await seedBlankDocument(user, team.id);
const documentV2 = await seedBlankDocument(user, team.id, { internalVersion: 2 });
await addSecondEnvelopeItem(documentV2.id);
const templateV1 = await seedBlankTemplate(user, team.id);
const templateV2 = await seedBlankTemplate(user, team.id, {
createTemplateOptions: { internalVersion: 2 },
});
await addSecondEnvelopeItem(templateV2.id);
await apiSignin({
page,
email: user.email,
redirectPath: `/t/${team.url}/documents/${documentV1.id}`,
});
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
await page.goto(`/t/${team.url}/documents/${documentV2.id}`);
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
await page.getByRole('button', { name: /Page 2/ }).click();
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
await page.goto(`/t/${team.url}/templates/${templateV1.id}`);
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
await page.goto(`/t/${team.url}/templates/${templateV2.id}`);
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
await page.getByRole('button', { name: /Page 2/ }).click();
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
await page.goto(`/t/${team.url}/documents/${documentV1.id}/edit`);
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
await page.goto(`/t/${team.url}/documents/${documentV2.id}/edit?step=addFields`);
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
await page.getByRole('button', { name: /Page 2/ }).click();
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
await page.goto(`/t/${team.url}/templates/${templateV1.id}/edit`);
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
await page.goto(`/t/${team.url}/templates/${templateV2.id}/edit?step=addFields`);
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
await page.getByRole('button', { name: /Page 2/ }).click();
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
await page.goto(`/t/${team.url}/documents/${documentV1.id}`);
await page.locator(PDF_PAGE_SELECTOR).first().waitFor({ state: 'visible', timeout: 30_000 });
});
});
test.describe('Recipient Signing', () => {
test('should render PDF on signing page (V1 and V2)', async ({ page }) => {
const { user, team } = await seedUser();
const { recipients: recipientsV1 } = await seedPendingDocumentWithFullFields({
owner: user,
teamId: team.id,
recipients: ['signer-v1@test.documenso.com'],
fields: [FieldType.SIGNATURE],
});
const { document: documentV2, recipients: recipientsV2 } =
await seedPendingDocumentWithFullFields({
owner: user,
teamId: team.id,
recipients: ['signer-v2@test.documenso.com'],
fields: [FieldType.SIGNATURE],
updateDocumentOptions: { internalVersion: 2 },
});
await addSecondEnvelopeItem(documentV2.id);
await page.goto(`/sign/${recipientsV1[0].token}`);
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
await page.goto(`/sign/${recipientsV2[0].token}`);
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
await page.getByRole('button', { name: /Page 2/ }).click();
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
});
});
test.describe('Direct Template', () => {
test('should render PDF on direct template page (V1 and V2)', async ({ page }) => {
const { user, team } = await seedUser();
const templateV1 = await seedDirectTemplate({
title: 'PDF Viewer Test Template V1',
userId: user.id,
teamId: team.id,
});
const templateV2 = await seedDirectTemplate({
title: 'PDF Viewer Test Template V2',
userId: user.id,
teamId: team.id,
internalVersion: 2,
});
await addSecondEnvelopeItem(templateV2.id);
await page.goto(formatDirectTemplatePath(templateV1.directLink?.token || ''));
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
await page.goto(formatDirectTemplatePath(templateV2.directLink?.token || ''));
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
await page.getByRole('button', { name: /Page 2/ }).click();
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
});
});
test.describe('Embed Pages', () => {
test('should render PDF on embed sign page (V1 and V2)', async ({ page }) => {
const { user, team } = await seedUser();
const { recipients: recipientsV1 } = await seedPendingDocumentWithFullFields({
owner: user,
teamId: team.id,
recipients: ['embed-signer-v1@test.documenso.com'],
fields: [FieldType.SIGNATURE],
});
const { document: documentV2, recipients: recipientsV2 } =
await seedPendingDocumentWithFullFields({
owner: user,
teamId: team.id,
recipients: ['embed-signer-v2@test.documenso.com'],
fields: [FieldType.SIGNATURE],
updateDocumentOptions: { internalVersion: 2 },
});
await addSecondEnvelopeItem(documentV2.id);
await page.goto(`/embed/sign/${recipientsV1[0].token}`);
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
await page.goto(`/embed/sign/${recipientsV2[0].token}`);
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
await page.getByRole('button', { name: /Page 2/ }).click();
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
});
test('should render PDF on embed direct template page (V1 and V2)', async ({ page }) => {
const { user, team } = await seedUser();
const templateV1 = await seedDirectTemplate({
title: 'Embed Direct Template V1',
userId: user.id,
teamId: team.id,
});
const templateV2 = await seedDirectTemplate({
title: 'Embed Direct Template V2',
userId: user.id,
teamId: team.id,
internalVersion: 2,
});
await addSecondEnvelopeItem(templateV2.id);
await page.goto(`/embed/direct/${templateV1.directLink?.token || ''}`);
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
await page.goto(`/embed/direct/${templateV2.directLink?.token || ''}`);
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
await page.getByRole('button', { name: /Page 2/ }).click();
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
});
test('should render PDF on embed authoring document create page', async ({ page }) => {
const { user, team } = await seedUser();
const { token: apiToken } = await createApiToken({
userId: user.id,
teamId: team.id,
tokenName: 'pdf-viewer-test',
expiresIn: null,
});
const { token: presignToken } = await createEmbeddingPresignToken({
apiToken,
});
const embedParams = { darkModeDisabled: false, features: {} };
const hash = btoa(encodeURIComponent(JSON.stringify(embedParams)));
await page.goto(
`${NEXT_PUBLIC_WEBAPP_URL()}/embed/v1/authoring/document/create?token=${presignToken}#${hash}`,
);
await expect(page.getByText('Configure Document')).toBeVisible({ timeout: 15_000 });
const titleInput = page.getByLabel('Title');
await titleInput.click();
await titleInput.fill('PDF Viewer E2E Test');
const emailInput = page.getByPlaceholder('Email').first();
await emailInput.click();
await emailInput.fill('test-signer@documenso.com');
const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser'),
page
.locator('input[type=file]')
.first()
.evaluate((el) => {
if (el instanceof HTMLInputElement) {
el.click();
}
}),
]);
await fileChooser.setFiles(path.join(__dirname, '../../../../assets/example.pdf'));
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.locator(PDF_PAGE_SELECTOR).first()).toBeVisible({ timeout: 30_000 });
});
});
});
@@ -42,21 +42,21 @@ const completeTemplateFlowWithDuplicateRecipients = async (options: {
// Step 3: Add fields for each recipient instance
// Add signature field for first instance
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 100, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 100, y: 100 } });
// Switch to second instance and add their field
await page.getByRole('combobox').first().click();
await page.getByText('Second Instance').first().click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 200, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 200, y: 100 } });
// Switch to different recipient and add their fields
await page.getByRole('combobox').first().click();
await page.getByText('Different Recipient').first().click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 300, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 300, y: 100 } });
await page.getByRole('button', { name: 'Name' }).click();
await page.locator('canvas').click({ position: { x: 300, y: 150 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 300, y: 150 } });
// Save template
await page.getByRole('button', { name: 'Save Template' }).click();
@@ -209,17 +209,17 @@ test.describe('[TEMPLATE_FLOW]: Duplicate Recipients', () => {
// Add fields for each recipient
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 100, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 100, y: 100 } });
await page.getByRole('combobox').first().click();
await page.getByText('Duplicate Recipient 2').first().click();
await page.getByRole('button', { name: 'Date' }).click();
await page.locator('canvas').click({ position: { x: 200, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 200, y: 100 } });
await page.getByRole('combobox').first().click();
await page.getByText('Different Recipient').first().click();
await page.getByRole('button', { name: 'Name' }).click();
await page.locator('canvas').click({ position: { x: 100, y: 200 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 100, y: 200 } });
// Save template
await page.getByRole('button', { name: 'Save Template' }).click();
@@ -272,7 +272,7 @@ test.describe('[TEMPLATE_FLOW]: Duplicate Recipients', () => {
await page.getByRole('combobox').first().click();
await page.getByRole('option', { name: 'First Instance' }).first().click();
await page.getByRole('button', { name: 'Name' }).click();
await page.locator('canvas').click({ position: { x: 100, y: 300 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 100, y: 300 } });
await page.waitForTimeout(2500);
@@ -1,6 +1,7 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
@@ -47,7 +48,7 @@ test.describe('AutoSave Fields Step', () => {
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -55,7 +56,7 @@ test.describe('AutoSave Fields Step', () => {
});
await page.getByRole('button', { name: 'Text' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -75,7 +76,7 @@ test.describe('AutoSave Fields Step', () => {
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 500,
@@ -110,7 +111,7 @@ test.describe('AutoSave Fields Step', () => {
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -118,7 +119,7 @@ test.describe('AutoSave Fields Step', () => {
});
await page.getByRole('button', { name: 'Text' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -138,7 +139,7 @@ test.describe('AutoSave Fields Step', () => {
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 500,
@@ -179,7 +180,7 @@ test.describe('AutoSave Fields Step', () => {
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -187,7 +188,7 @@ test.describe('AutoSave Fields Step', () => {
});
await page.getByRole('button', { name: 'Text' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -207,7 +208,7 @@ test.describe('AutoSave Fields Step', () => {
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 500,
@@ -250,7 +251,7 @@ test.describe('AutoSave Fields Step', () => {
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -258,7 +259,7 @@ test.describe('AutoSave Fields Step', () => {
});
await page.getByRole('button', { name: 'Text' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -2,6 +2,7 @@ import { expect, test } from '@playwright/test';
import { WebhookCallStatus, WebhookTriggerEvents } from '@prisma/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { alphaid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
@@ -279,8 +280,8 @@ test('[WEBHOOKS]: cannot see unrelated webhooks', async ({ page }) => {
const user1Data = await seedUser();
const user2Data = await seedUser();
const webhookUrl1 = `https://example.com/webhook-team1-${Date.now()}`;
const webhookUrl2 = `https://example.com/webhook-team2-${Date.now()}`;
const webhookUrl1 = `https://example.com/webhook-team1-${alphaid(12)}`;
const webhookUrl2 = `https://example.com/webhook-team2-${alphaid(12)}`;
// Create webhooks for both teams with DOCUMENT_CREATED event
const webhook1 = await seedWebhook({
@@ -142,6 +142,7 @@ export const createEmailDomain = async ({ domain, organisationId }: CreateEmailD
publicKey: true,
createdAt: true,
updatedAt: true,
lastVerifiedAt: true,
emails: true,
},
});
@@ -0,0 +1,93 @@
import { DeleteEmailIdentityCommand } from '@aws-sdk/client-sesv2';
import { EmailDomainStatus } from '@prisma/client';
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { prisma } from '@documenso/prisma';
import { getSesClient, verifyDomainWithDKIM } from './create-email-domain';
type ReregisterEmailDomainOptions = {
emailDomainId: string;
};
/**
* Re-register an email domain in SES using the same DKIM key pair.
*
* This deletes the existing SES identity and recreates it with the same
* selector and private key, so the user does not need to update their DNS records.
*
* Permission is assumed to be checked in the caller.
*/
export const reregisterEmailDomain = async ({ emailDomainId }: ReregisterEmailDomainOptions) => {
const encryptionKey = DOCUMENSO_ENCRYPTION_KEY;
if (!encryptionKey) {
throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY');
}
const emailDomain = await prisma.emailDomain.findUnique({
where: {
id: emailDomainId,
},
});
if (!emailDomain) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Email domain not found',
});
}
const sesClient = getSesClient();
// Delete the existing SES identity, ignoring if it no longer exists.
await sesClient
.send(
new DeleteEmailIdentityCommand({
EmailIdentity: emailDomain.domain,
}),
)
.catch((err) => {
if (err.name === 'NotFoundException') {
return;
}
throw err;
});
// Decrypt the stored private key.
const decryptedPrivateKeyBytes = symmetricDecrypt({
key: encryptionKey,
data: emailDomain.privateKey,
});
const decryptedPrivateKey = new TextDecoder().decode(decryptedPrivateKeyBytes);
// The selector field in the DB is the full record name (e.g. "documenso-orgid._domainkey.example.com").
// We need to extract just the selector part (before "._domainkey.").
const selectorParts = emailDomain.selector.split('._domainkey.');
const selector = selectorParts[0];
if (!selector) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Could not extract selector from email domain record',
});
}
// Recreate the SES identity with the same DKIM key pair.
await verifyDomainWithDKIM(emailDomain.domain, selector, decryptedPrivateKey);
// Reset status to PENDING and update lastVerifiedAt.
const updatedEmailDomain = await prisma.emailDomain.update({
where: {
id: emailDomainId,
},
data: {
status: EmailDomainStatus.PENDING,
lastVerifiedAt: new Date(),
},
});
return updatedEmailDomain;
};
@@ -35,6 +35,7 @@ export const verifyEmailDomain = async (emailDomainId: string) => {
},
data: {
status: isVerified ? EmailDomainStatus.ACTIVE : EmailDomainStatus.PENDING,
lastVerifiedAt: new Date(),
},
});
@@ -1,4 +1,4 @@
export const getBoundingClientRect = (element: HTMLElement) => {
export const getBoundingClientRect = (element: HTMLElement | Element) => {
const rect = element.getBoundingClientRect();
const { width, height } = rect;
@@ -14,7 +14,10 @@ export const useDocumentElement = () => {
const target = event.target;
const $page =
target.closest<HTMLElement>(pageSelector) ?? target.querySelector<HTMLElement>(pageSelector);
target.closest<HTMLElement>(pageSelector) ??
document
.elementsFromPoint(event.clientX, event.clientY)
.find((el) => el.matches(pageSelector));
if (!$page) {
return null;
@@ -17,7 +17,7 @@ export const useElementBounds = (elementOrSelector: HTMLElement | string, withSc
: elementOrSelector;
if (!$el) {
throw new Error('Element not found');
return { top: 0, left: 0, width: 0, height: 0 };
}
if (withScroll) {
@@ -57,23 +57,64 @@ export const useFieldPageCoords = (
};
}, [calculateCoords]);
// Watch for the page element to appear in the DOM (e.g. after a virtual list
// scroll) and recalculate coords. Also attach a ResizeObserver once the page
// element exists.
useEffect(() => {
const $page = document.querySelector<HTMLElement>(
`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.page}"]`,
);
const pageSelector = `${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.page}"]`;
if (!$page) {
return;
let resizeObserver: ResizeObserver | null = null;
let observedElement: HTMLElement | null = null;
const attachResizeObserver = ($page: HTMLElement) => {
if ($page === observedElement) {
return;
}
resizeObserver?.disconnect();
resizeObserver = new ResizeObserver(() => {
calculateCoords();
});
resizeObserver.observe($page);
observedElement = $page;
};
// Try to attach immediately if the page already exists.
const existingPage = document.querySelector<HTMLElement>(pageSelector);
if (existingPage) {
attachResizeObserver(existingPage);
}
const observer = new ResizeObserver(() => {
// Watch for DOM mutations to detect when the page element appears (e.g.
// after the virtual list scrolls to a new page and renders it).
const mutationObserver = new MutationObserver(() => {
const $page = document.querySelector<HTMLElement>(pageSelector);
if (!$page) {
return;
}
// Only recalculate when the observed page element has changed (e.g. new
// element appeared after virtual list scroll). Skip when mutations are
// from elsewhere in the DOM and the page element is unchanged.
if ($page === observedElement) {
return;
}
calculateCoords();
attachResizeObserver($page);
});
observer.observe($page);
mutationObserver.observe(document.body, {
childList: true,
subtree: true,
});
return () => {
observer.disconnect();
mutationObserver.disconnect();
resizeObserver?.disconnect();
observedElement = null;
};
}, [calculateCoords, field.page]);
@@ -0,0 +1,34 @@
import { useEffect, useState } from 'react';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
/**
* Returns whether the PDF page element for the given page number is currently
* present in the DOM. With virtual list rendering only pages near the viewport
* are mounted, so this hook lets consumers skip rendering when their page is
* virtualised away.
*/
export const useIsPageInDom = (pageNumber: number) => {
const [isPageInDom, setIsPageInDom] = useState(false);
useEffect(() => {
const selector = `${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${pageNumber}"]`;
setIsPageInDom(document.querySelector(selector) !== null);
const observer = new MutationObserver(() => {
setIsPageInDom(document.querySelector(selector) !== null);
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
return () => {
observer.disconnect();
};
}, [pageNumber]);
return isPageInDom;
};
@@ -1,46 +1,45 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import Konva from 'konva';
import type { RenderParameters } from 'pdfjs-dist/types/src/display/api';
import { usePageContext } from 'react-pdf';
import { PDF_VIEWER_PAGE_CLASSNAME } from '@documenso/lib/constants/pdf-viewer';
import { EAGER_LOAD_PAGE_COUNT, type PageRenderData } from '../providers/envelope-render-provider';
type RenderFunction = (props: { stage: Konva.Stage; pageLayer: Konva.Layer }) => void;
export function usePageRenderer(renderFunction: RenderFunction) {
const pageContext = usePageContext();
export const usePageRenderer = (renderFunction: RenderFunction, pageData: PageRenderData) => {
const { pageWidth, pageHeight, scale, imageUrl, pageNumber } = pageData;
if (!pageContext) {
throw new Error('Unable to find Page context.');
}
const { page, rotate, scale } = pageContext;
if (!page) {
throw new Error('Attempted to render page canvas, but no page was specified.');
}
const canvasElement = useRef<HTMLCanvasElement>(null);
const konvaContainer = useRef<HTMLDivElement>(null);
const stage = useRef<Konva.Stage | null>(null);
const pageLayer = useRef<Konva.Layer | null>(null);
const [renderError, setRenderError] = useState<boolean>(false);
const [renderStatus, setRenderStatus] = useState<'loading' | 'loaded' | 'error'>('loading');
/**
* The raw viewport with no scaling. Basically the actual PDF size.
*/
const unscaledViewport = useMemo(
() => page.getViewport({ scale: 1, rotation: rotate }),
[page, rotate, scale],
() => ({
scale: 1,
width: pageWidth,
height: pageHeight,
}),
[pageWidth, pageHeight],
);
/**
* The viewport scaled according to page width.
*/
const scaledViewport = useMemo(
() => page.getViewport({ scale, rotation: rotate }),
[page, rotate, scale],
() => ({
scale,
width: pageWidth * scale,
height: pageHeight * scale,
}),
[pageWidth, pageHeight, scale],
);
/**
@@ -48,88 +47,77 @@ export function usePageRenderer(renderFunction: RenderFunction) {
* in a higher resolution.
*/
const renderViewport = useMemo(
() => page.getViewport({ scale: scale * window.devicePixelRatio, rotation: rotate }),
[page, rotate, scale],
() => ({
scale: scale * window.devicePixelRatio,
width: pageWidth * scale * window.devicePixelRatio,
height: pageHeight * scale * window.devicePixelRatio,
}),
[pageWidth, pageHeight, scale],
);
/**
* Render the PDF and create the scaled Konva stage.
* The props for the image element which will render the page.
*/
useEffect(
function drawPageOnCanvas() {
if (!page) {
return;
}
const { current: canvas } = canvasElement;
const { current: kContainer } = konvaContainer;
if (!canvas || !kContainer) {
return;
}
canvas.width = renderViewport.width;
canvas.height = renderViewport.height;
canvas.style.width = `${Math.floor(scaledViewport.width)}px`;
canvas.style.height = `${Math.floor(scaledViewport.height)}px`;
const renderContext: RenderParameters = {
canvas,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
canvasContext: canvas.getContext('2d', { alpha: false }) as CanvasRenderingContext2D,
viewport: renderViewport,
};
const cancellable = page.render(renderContext);
const runningTask = cancellable;
cancellable.promise.catch(() => {
// Intentionally empty
});
void cancellable.promise.then(() => {
stage.current = new Konva.Stage({
container: kContainer,
width: scaledViewport.width,
height: scaledViewport.height,
scale: {
x: scale,
y: scale,
},
});
// Create the main layer for interactive elements.
pageLayer.current = new Konva.Layer();
stage.current.add(pageLayer.current);
renderFunction({
stage: stage.current,
pageLayer: pageLayer.current,
});
void document.fonts.ready.then(function () {
pageLayer.current?.batchDraw();
});
});
return () => {
runningTask.cancel();
};
},
[page, scaledViewport],
const imageProps = useMemo(
(): React.ImgHTMLAttributes<HTMLImageElement> & Record<string, unknown> & { alt: '' } => ({
className: PDF_VIEWER_PAGE_CLASSNAME,
width: Math.floor(scaledViewport.width),
height: Math.floor(scaledViewport.height),
alt: '',
onLoad: () => setRenderStatus('loaded'),
// Purposely not using lazy here since we can use the virtual list overscan to let us prerender images.
loading: pageNumber < EAGER_LOAD_PAGE_COUNT ? 'eager' : undefined,
src: imageUrl,
'data-page-number': pageNumber,
}),
[renderViewport, scaledViewport, imageUrl],
);
useEffect(() => {
const { current: container } = konvaContainer;
if (renderStatus !== 'loaded' || !container) {
return;
}
stage.current = new Konva.Stage({
container,
width: scaledViewport.width,
height: scaledViewport.height,
scale: {
x: scale,
y: scale,
},
});
// Create the main layer for interactive elements.
pageLayer.current = new Konva.Layer();
stage.current.add(pageLayer.current);
renderFunction({
stage: stage.current,
pageLayer: pageLayer.current,
});
void document.fonts.ready.then(function () {
pageLayer.current?.batchDraw();
});
return () => {
stage.current?.destroy();
stage.current = null;
};
}, [renderStatus, imageProps]);
return {
canvasElement,
konvaContainer,
imageProps,
stage,
pageLayer,
unscaledViewport,
scaledViewport,
pageContext,
renderError,
setRenderError,
renderStatus,
setRenderStatus,
};
}
};
@@ -1,23 +1,49 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import React from 'react';
import type { Field, Recipient } from '@prisma/client';
import { DocumentDataType, type Field, type Recipient } from '@prisma/client';
import pMap from 'p-map';
import { PDF_IMAGE_RENDER_SCALE } from '@documenso/lib/constants/pdf-viewer';
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
import type { TGetEnvelopeItemsMetaResponse } from '@documenso/remix/server/api/files/files.types';
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
import { AVAILABLE_RECIPIENT_COLORS } from '@documenso/ui/lib/recipient-colors';
import type { DocumentDataVersion } from '../../types/document-data';
import type { TEnvelope } from '../../types/envelope';
import type { FieldRenderMode } from '../../universal/field-renderer/render-field';
import { getEnvelopeItemPdfUrl } from '../../utils/envelope-download';
import { getEnvelopeItemMetaUrl, getEnvelopeItemPageImageUrl } from '../../utils/envelope-images';
type FileData =
| {
status: 'loading' | 'error';
}
| {
file: Uint8Array;
status: 'loaded';
};
/**
* Number of pages to load eagerly on initial render.
* Pages beyond this threshold will be loaded lazily when they enter the viewport.
*/
export const EAGER_LOAD_PAGE_COUNT = 5;
export type PageRenderData = BasePageRenderData & {
scale: number;
};
export type BasePageRenderData = {
envelopeItemId: string;
documentDataId: string;
pageIndex: number;
pageNumber: number;
pageWidth: number;
pageHeight: number;
imageUrl: string;
};
export type ImageLoadingState = 'loading' | 'loaded' | 'error';
type EnvelopeRenderOverrideSettings = {
mode?: FieldRenderMode;
@@ -25,11 +51,19 @@ type EnvelopeRenderOverrideSettings = {
showRecipientSigningStatus?: boolean;
};
type EnvelopeRenderItem = TEnvelope['envelopeItems'][number];
type EnvelopeRenderItem = {
id: string;
title: string;
order: number;
envelopeId: string;
data?: Uint8Array | null;
};
type EnvelopeRenderProviderValue = {
getPdfBuffer: (envelopeItemId: string) => FileData | null;
version: DocumentDataVersion;
envelopeItems: EnvelopeRenderItem[];
envelopeItemsMeta: Record<string, BasePageRenderData[]>;
envelopeItemsMetaLoadingState: ImageLoadingState;
envelopeStatus: TEnvelope['status'];
envelopeType: TEnvelope['type'];
currentEnvelopeItem: EnvelopeRenderItem | null;
@@ -46,7 +80,17 @@ type EnvelopeRenderProviderValue = {
interface EnvelopeRenderProviderProps {
children: React.ReactNode;
envelope: Pick<TEnvelope, 'envelopeItems' | 'status' | 'type'>;
/**
* The envelope item version to render.
*/
version: DocumentDataVersion;
envelope: Pick<TEnvelope, 'id' | 'status' | 'type'>;
/**
* The envelope items to render.
*/
envelopeItems: EnvelopeRenderItem[];
/**
* Optional fields which are passed down to renderers for custom rendering needs.
@@ -70,6 +114,13 @@ interface EnvelopeRenderProviderProps {
*/
token: string | undefined;
/**
* The presign token to access the envelope.
*
* If not provided, it will be assumed that the current user can access the document.
*/
presignToken?: string | undefined;
/**
* Custom override settings for generic page renderers.
*/
@@ -89,81 +140,251 @@ export const useCurrentEnvelopeRender = () => {
};
/**
* Manages fetching and storing PDF files to render on the client.
* Manages fetching the data required to render an envelope and it's items.
*/
export const EnvelopeRenderProvider = ({
children,
envelope,
envelopeItems: envelopeItemsFromProps,
fields,
token,
presignToken,
recipients = [],
version,
overrideSettings,
}: EnvelopeRenderProviderProps) => {
// Indexed by documentDataId.
const [files, setFiles] = useState<Record<string, FileData>>({});
// Indexed by envelope item ID.
const [envelopeItemsMeta, setEnvelopeItemsMeta] = useState<Record<string, BasePageRenderData[]>>(
{},
);
const [currentItem, setCurrentItem] = useState<EnvelopeRenderItem | null>(null);
const [envelopeItemsMetaLoadingState, setEnvelopeItemsMetaLoadingState] =
useState<ImageLoadingState>('loading');
const [renderError, setRenderError] = useState<boolean>(false);
// Track the timestamp of the most recent fetch to prevent race conditions
const fetchStartedAtRef = useRef<number>(0);
const envelopeItems = useMemo(
() => envelope.envelopeItems.sort((a, b) => a.order - b.order),
[envelope.envelopeItems],
() => [...envelopeItemsFromProps].sort((a, b) => a.order - b.order),
[envelopeItemsFromProps],
);
const loadEnvelopeItemPdfFile = async (envelopeItem: EnvelopeRenderItem) => {
if (files[envelopeItem.id]?.status === 'loading') {
const [currentItem, setCurrentItem] = useState<EnvelopeRenderItem | null>(
envelopeItems[0] ?? null,
);
/**
* Fetch metadata and preload initial images when the envelope or token changes.
*/
useEffect(() => {
void fetchEnvelopeRenderData();
}, [envelope.id, envelopeItems, token, version, presignToken]);
const fetchEnvelopeRenderData = useCallback(async () => {
if (envelopeItems.length === 0) {
setEnvelopeItemsMetaLoadingState('loaded');
return;
}
if (!files[envelopeItem.id]) {
setFiles((prev) => ({
...prev,
[envelopeItem.id]: {
status: 'loading',
},
}));
// Handle "create" embedding mode since all the files are local in that scenario.
if (!envelope.id) {
await handleLocalFileFetch();
return;
}
// Record when this fetch started to detect stale responses
const fetchStartedAt = Date.now();
fetchStartedAtRef.current = fetchStartedAt;
setEnvelopeItemsMetaLoadingState('loading');
try {
const downloadUrl = getEnvelopeItemPdfUrl({
type: 'view',
envelopeItem: envelopeItem,
// Fetch metadata for all envelope items
const metaUrl = getEnvelopeItemMetaUrl({
envelopeId: envelope.id,
token,
presignToken,
});
const blob = await fetch(downloadUrl).then(async (res) => await res.blob());
const response = await fetch(metaUrl);
const file = await blob.arrayBuffer();
if (!response.ok) {
throw new Error(`Failed to fetch envelope meta: ${response.status}`);
}
setFiles((prev) => ({
...prev,
[envelopeItem.id]: {
file: new Uint8Array(file),
status: 'loaded',
const data: TGetEnvelopeItemsMetaResponse = await response.json();
// Check again after parsing JSON in case a newer fetch started
if (fetchStartedAtRef.current !== fetchStartedAt) {
return;
}
// Build a map of envelope items by ID
const metaMap: Record<string, BasePageRenderData[]> = {};
const s3EnvelopeItems = data.envelopeItems.filter(
(item) => item.documentDataType === DocumentDataType.S3_PATH,
);
const localPdfs: { envelopeItemId: string; documentDataId: string; data: Uint8Array }[] = [];
// Handle local envelope items from embedding flows.
for (const item of envelopeItems) {
if (item.data) {
localPdfs.push({
envelopeItemId: item.id,
documentDataId: item.id,
data: new Uint8Array(item.data as Uint8Array),
});
}
}
const bytes64EnvelopeItems = data.envelopeItems.filter(
(item) => item.documentDataType !== DocumentDataType.S3_PATH,
);
// Handle byte64 envelope items from the database.
const pdfs = await pMap(
bytes64EnvelopeItems,
async (item) => {
const envelopeItemPdfUrl = getEnvelopeItemPdfUrl({
type: 'view',
envelopeItem: {
id: item.envelopeItemId,
envelopeId: envelope.id,
},
token,
presignToken,
});
const response = await fetch(envelopeItemPdfUrl);
if (!response.ok) {
throw new Error(`Failed to fetch envelope item PDF: ${response.status}`);
}
const pdfBytes = await response.arrayBuffer();
return {
envelopeItemId: item.envelopeItemId,
documentDataId: item.documentDataId,
data: new Uint8Array(pdfBytes),
};
},
}));
{ concurrency: 5 },
);
localPdfs.push(...pdfs);
for (const item of s3EnvelopeItems) {
metaMap[item.envelopeItemId] = item.pages.map((page, pageIndex) => {
const imageUrl = getEnvelopeItemPageImageUrl({
envelopeId: envelope.id,
envelopeItemId: item.envelopeItemId,
documentDataId: item.documentDataId,
pageIndex,
token,
presignToken,
version,
});
return {
envelopeItemId: item.envelopeItemId,
documentDataId: item.documentDataId,
pageIndex,
pageNumber: pageIndex + 1,
pageWidth: page.originalWidth,
pageHeight: page.originalHeight,
imageUrl,
};
});
}
if (localPdfs.length > 0) {
// Dynamically import "pdfToImagesClientSide" function to prevent bundling.
const { pdfToImagesClientSide } = await import(
'@documenso/lib/server-only/ai/pdf-to-images.client'
);
await pMap(
localPdfs,
async (item) => {
const pdfImages = await pdfToImagesClientSide(item.data, {
scale: PDF_IMAGE_RENDER_SCALE,
});
metaMap[item.envelopeItemId] = pdfImages.map((image) => ({
envelopeItemId: item.envelopeItemId,
documentDataId: item.documentDataId,
pageIndex: image.pageIndex,
pageNumber: image.pageIndex + 1,
pageWidth: image.width,
pageHeight: image.height,
imageUrl: image.image,
}));
},
{ concurrency: 10 },
);
}
setEnvelopeItemsMeta(metaMap);
setEnvelopeItemsMetaLoadingState('loaded');
} catch (error) {
console.error(error);
// Only set error state if this is still the most recent fetch
if (fetchStartedAtRef.current === fetchStartedAt) {
console.error('Failed to load envelope data:', error);
setEnvelopeItemsMetaLoadingState('error');
}
}
}, [envelope.id, envelopeItems, token, version]);
setFiles((prev) => ({
...prev,
[envelopeItem.id]: {
status: 'error',
},
}));
const handleLocalFileFetch = async () => {
setEnvelopeItemsMetaLoadingState('loading');
try {
// Build a map of envelope items by ID
const metaMap: Record<string, BasePageRenderData[]> = {};
// Dynamically import "pdfToImagesClientSide" function to prevent bundling.
const { pdfToImagesClientSide } = await import(
'@documenso/lib/server-only/ai/pdf-to-images.client'
);
for (const item of envelopeItems) {
if (item.data) {
// Clone the buffer so PDF.js can transfer it to its worker without detaching the one in state
const pdfBytes = new Uint8Array(structuredClone(item.data));
const pdfImages = await pdfToImagesClientSide(pdfBytes, {
scale: PDF_IMAGE_RENDER_SCALE,
});
metaMap[item.id] = pdfImages.map((image) => ({
envelopeItemId: item.id,
documentDataId: item.id,
pageIndex: image.pageIndex,
pageNumber: image.pageIndex + 1,
pageWidth: image.width,
pageHeight: image.height,
imageUrl: image.image,
}));
}
}
setEnvelopeItemsMeta(metaMap);
setEnvelopeItemsMetaLoadingState('loaded');
} catch (error) {
console.error('Failed to load envelope data:', error);
setEnvelopeItemsMetaLoadingState('error');
}
};
const getPdfBuffer = useCallback(
(envelopeItemId: string) => {
return files[envelopeItemId] || null;
},
[files],
);
const setCurrentEnvelopeItem = (envelopeItemId: string) => {
const foundItem = envelope.envelopeItems.find((item) => item.id === envelopeItemId);
const foundItem = envelopeItems.find((item) => item.id === envelopeItemId);
setCurrentItem(foundItem ?? null);
};
@@ -179,15 +400,6 @@ export const EnvelopeRenderProvider = ({
}
}, [currentItem, envelopeItems]);
// Look for any missing pdf files and load them.
useEffect(() => {
const missingFiles = envelope.envelopeItems.filter((item) => !files[item.id]);
for (const item of missingFiles) {
void loadEnvelopeItemPdfFile(item);
}
}, [envelope.envelopeItems]);
const recipientIds = useMemo(
() => recipients.map((recipient) => recipient.id).sort(),
[recipients],
@@ -207,7 +419,9 @@ export const EnvelopeRenderProvider = ({
return (
<EnvelopeRenderContext.Provider
value={{
getPdfBuffer,
version,
envelopeItemsMeta,
envelopeItemsMetaLoadingState,
envelopeItems,
envelopeStatus: envelope.status,
envelopeType: envelope.type,
+22
View File
@@ -0,0 +1,22 @@
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
// This is separate from the pdf-viewer.ts constant file due to parsing issues during testing.
export const PDF_VIEWER_ERROR_MESSAGES = {
editor: {
title: msg`Configuration Error`,
description: msg`There was an issue rendering some fields, please review the fields and try again.`,
},
preview: {
title: msg`Configuration Error`,
description: msg`Something went wrong while rendering the document, some fields may be missing or corrupted.`,
},
signing: {
title: msg`Configuration Error`,
description: msg`Something went wrong while rendering the document, some fields may be missing or corrupted.`,
},
default: {
title: msg`Configuration Error`,
description: msg`Something went wrong while rendering the document, please try again or contact our support.`,
},
} satisfies Record<string, { title: MessageDescriptor; description: MessageDescriptor }>;
+7 -1
View File
@@ -1,2 +1,8 @@
export const PDF_VIEWER_CONTAINER_SELECTOR = '.react-pdf__Document';
// Keep these two constants in sync.
export const PDF_VIEWER_PAGE_SELECTOR = '.react-pdf__Page';
export const PDF_VIEWER_PAGE_CLASSNAME = 'react-pdf__Page z-0';
/**
* Changing this will require large testing.
*/
export const PDF_IMAGE_RENDER_SCALE = 2;
+2
View File
@@ -16,6 +16,7 @@ import { EXECUTE_WEBHOOK_JOB_DEFINITION } from './definitions/internal/execute-w
import { EXPIRE_RECIPIENTS_SWEEP_JOB_DEFINITION } from './definitions/internal/expire-recipients-sweep';
import { PROCESS_RECIPIENT_EXPIRED_JOB_DEFINITION } from './definitions/internal/process-recipient-expired';
import { SEAL_DOCUMENT_JOB_DEFINITION } from './definitions/internal/seal-document';
import { SYNC_EMAIL_DOMAINS_JOB_DEFINITION } from './definitions/internal/sync-email-domains';
/**
* The `as const` assertion is load bearing as it provides the correct level of type inference for
@@ -39,6 +40,7 @@ export const jobsClient = new JobClient([
EXPIRE_RECIPIENTS_SWEEP_JOB_DEFINITION,
PROCESS_RECIPIENT_EXPIRED_JOB_DEFINITION,
CLEANUP_RATE_LIMITS_JOB_DEFINITION,
SYNC_EMAIL_DOMAINS_JOB_DEFINITION,
] as const);
export const jobs = jobsClient;
+5 -1
View File
@@ -26,6 +26,7 @@ export class InngestJobProvider extends BaseJobProvider {
const client = new InngestClient({
id: env('NEXT_PRIVATE_INNGEST_APP_ID') || 'documenso-app',
eventKey: env('INNGEST_EVENT_KEY') || env('NEXT_PRIVATE_INNGEST_EVENT_KEY'),
logger: console,
});
this._instance = new InngestJobProvider({ client });
@@ -90,7 +91,10 @@ export class InngestJobProvider extends BaseJobProvider {
return {
wait: step.sleep,
logger: {
...ctx.logger,
info: ctx.logger.info,
debug: ctx.logger.debug,
error: ctx.logger.error,
warn: ctx.logger.warn,
log: ctx.logger.info,
},
runTask: async (cacheKey, callback) => {
@@ -285,18 +285,13 @@ export const run = async ({
await prisma.$transaction(async (tx) => {
for (const { oldDocumentDataId, newDocumentDataId } of newDocumentData) {
const newData = await tx.documentData.findFirstOrThrow({
await tx.envelopeItem.update({
where: {
id: newDocumentDataId,
},
});
await tx.documentData.update({
where: {
id: oldDocumentDataId,
envelopeId: envelope.id,
documentDataId: oldDocumentDataId,
},
data: {
data: newData.data,
documentDataId: newDocumentDataId,
},
});
}
@@ -496,11 +491,14 @@ const decorateAndSignPdf = async ({
// Add suffix based on document status
const suffix = isRejected ? '_rejected.pdf' : '_signed.pdf';
const newDocumentData = await putPdfFileServerSide({
name: `${name}${suffix}`,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(pdfBytes),
});
const newDocumentData = await putPdfFileServerSide(
{
name: `${name}${suffix}`,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(pdfBytes),
},
envelopeItem.documentData.initialData,
);
return {
oldDocumentDataId: envelopeItem.documentData.id,
@@ -0,0 +1,86 @@
import { DateTime } from 'luxon';
import { reregisterEmailDomain } from '@documenso/ee/server-only/lib/reregister-email-domain';
import { verifyEmailDomain } from '@documenso/ee/server-only/lib/verify-email-domain';
import { prisma } from '@documenso/prisma';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSyncEmailDomainsJobDefinition } from './sync-email-domains';
const BATCH_SIZE = 10;
const AUTO_REREGISTER_AFTER_HOURS = 48;
export const run = async ({ io }: { payload: TSyncEmailDomainsJobDefinition; io: JobRunIO }) => {
const pendingDomains = await prisma.emailDomain.findMany({
where: {
status: 'PENDING',
},
select: {
id: true,
domain: true,
createdAt: true,
lastVerifiedAt: true,
},
orderBy: {
lastVerifiedAt: { sort: 'asc', nulls: 'first' },
},
});
if (pendingDomains.length === 0) {
io.logger.info('No pending email domains to sync');
return;
}
io.logger.info(`Found ${pendingDomains.length} pending email domains to sync`);
let verifiedCount = 0;
let reregisteredCount = 0;
let errorCount = 0;
const reregisterCutoff = DateTime.now().minus({ hours: AUTO_REREGISTER_AFTER_HOURS }).toJSDate();
for (let i = 0; i < pendingDomains.length; i += BATCH_SIZE) {
const batch = pendingDomains.slice(i, i + BATCH_SIZE);
const results = await Promise.allSettled(
batch.map(async (domain) => {
const shouldReregister = domain.createdAt < reregisterCutoff;
if (shouldReregister) {
io.logger.info(
`Domain "${domain.domain}" has been pending since ${domain.createdAt.toISOString()}, attempting re-registration`,
);
await reregisterEmailDomain({ emailDomainId: domain.id });
return 'reregistered' as const;
}
const { isVerified } = await verifyEmailDomain(domain.id);
return isVerified ? ('verified' as const) : ('pending' as const);
}),
);
for (const result of results) {
if (result.status === 'rejected') {
errorCount++;
io.logger.error(`Failed to process email domain: ${String(result.reason)}`);
} else if (result.value === 'verified') {
verifiedCount++;
} else if (result.value === 'reregistered') {
reregisteredCount++;
}
}
// Small delay between batches to respect SES API rate limits.
if (i + BATCH_SIZE < pendingDomains.length) {
await new Promise<void>((resolve) => {
setTimeout(resolve, 1000);
});
}
}
io.logger.info(
`Sync complete: ${verifiedCount} verified, ${reregisteredCount} re-registered, ${errorCount} errors out of ${pendingDomains.length} pending domains`,
);
};
@@ -0,0 +1,30 @@
import { z } from 'zod';
import { type JobDefinition } from '../../client/_internal/job';
const SYNC_EMAIL_DOMAINS_JOB_DEFINITION_ID = 'internal.sync-email-domains';
const SYNC_EMAIL_DOMAINS_JOB_DEFINITION_SCHEMA = z.object({});
export type TSyncEmailDomainsJobDefinition = z.infer<
typeof SYNC_EMAIL_DOMAINS_JOB_DEFINITION_SCHEMA
>;
export const SYNC_EMAIL_DOMAINS_JOB_DEFINITION = {
id: SYNC_EMAIL_DOMAINS_JOB_DEFINITION_ID,
name: 'Sync Email Domains',
version: '1.0.0',
trigger: {
name: SYNC_EMAIL_DOMAINS_JOB_DEFINITION_ID,
schema: SYNC_EMAIL_DOMAINS_JOB_DEFINITION_SCHEMA,
cron: '0 * * * *', // Every hour, on the hour.
},
handler: async ({ payload, io }) => {
const handler = await import('./sync-email-domains.handler');
await handler.run({ payload, io });
},
} as const satisfies JobDefinition<
typeof SYNC_EMAIL_DOMAINS_JOB_DEFINITION_ID,
TSyncEmailDomainsJobDefinition
>;
-1
View File
@@ -55,7 +55,6 @@
"posthog-js": "^1.297.2",
"posthog-node": "4.18.0",
"react": "^18",
"react-pdf": "^10.3.0",
"remeda": "^2.32.0",
"sharp": "0.34.5",
"skia-canvas": "^3.0.8",
@@ -162,8 +162,8 @@ export const detectFieldsFromPdf = async ({
// Mask existing fields on the image
const maskedImage = await maskFieldsOnImage({
image: page.image,
width: page.width,
height: page.height,
width: page.scaledWidth,
height: page.scaledHeight,
fields: fieldsOnPage,
});
@@ -0,0 +1,73 @@
import pMap from 'p-map';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/legacy/build/pdf.worker.min.mjs',
import.meta.url,
).toString();
export type PdfToImagesOptions = {
scale?: number;
};
export type PdfImageResult = {
pageIndex: number;
image: string;
width: number;
height: number;
};
export const pdfToImagesClientSide = async (
pdfBytes: Uint8Array,
options: PdfToImagesOptions = {},
): Promise<PdfImageResult[]> => {
const { scale = 2 } = options;
const task = pdfjsLib.getDocument({
data: pdfBytes,
});
const pdf = await task.promise;
const images = await pMap(
Array.from({ length: pdf.numPages }),
async (_, pageIndex) => {
const pageNumber = pageIndex + 1;
const page = await pdf.getPage(pageNumber);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
canvas.width = Math.floor(viewport.width);
canvas.height = Math.floor(viewport.height);
const context = canvas.getContext('2d');
if (!context) {
throw new Error('Failed to get 2D canvas context');
}
await page.render({
canvasContext: context,
viewport,
canvas,
}).promise;
const imageBase64 = canvas.toDataURL('image/jpeg');
page.cleanup();
return {
pageIndex,
image: imageBase64,
width: canvas.width,
height: canvas.height,
};
},
{ concurrency: 50 },
);
await pdf.destroy();
await task.destroy();
return images;
};
+94 -32
View File
@@ -1,7 +1,11 @@
import pMap from 'p-map';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
import type { PDFDocumentProxy } from 'pdfjs-dist/types/src/display/api';
import type { ExportFormat } from 'skia-canvas';
import { Canvas, Image, Path2D } from 'skia-canvas';
import { PDF_IMAGE_RENDER_SCALE } from '../../constants/pdf-viewer';
// @ts-expect-error napi-rs/canvas satisfies the requirements
globalThis.Path2D = Path2D;
// @ts-expect-error napi-rs/canvas satisfies the requirements
@@ -42,10 +46,17 @@ class SkiaCanvasFactory {
export type PdfToImagesOptions = {
scale?: number;
/**
* The format of the images to return.
*
* Defaults to 'jpeg'.
*/
imageFormat?: ExportFormat;
};
export const pdfToImages = async (pdfBytes: Uint8Array, options: PdfToImagesOptions = {}) => {
const { scale = 2 } = options;
const { scale = PDF_IMAGE_RENDER_SCALE, imageFormat = 'jpeg' } = options;
const task = await pdfjsLib.getDocument({
data: pdfBytes,
@@ -56,37 +67,7 @@ export const pdfToImages = async (pdfBytes: Uint8Array, options: PdfToImagesOpti
const images = await pMap(
Array.from({ length: pdf.numPages }),
async (_, index) => {
const pageNumber = index + 1;
const page = await pdf.getPage(pageNumber);
const viewport = page.getViewport({ scale });
const canvas = new Canvas(viewport.width, viewport.height);
canvas.gpu = false;
const canvasContext = canvas.getContext('2d');
await page.render({
// @ts-expect-error napi-rs/canvas satifies the requirements
canvas,
// @ts-expect-error napi-rs/canvas satifies the requirements
canvasContext,
viewport,
}).promise;
const result = {
pageNumber,
image: await canvas.toBuffer('jpeg'),
width: Math.floor(viewport.width),
height: Math.floor(viewport.height),
mimeType: 'image/jpeg',
};
void page.cleanup();
return result;
},
async (_, pageIndex) => getPdfPageAsImage({ pdf, pageIndex, scale, imageFormat }),
{ concurrency: 10 },
);
@@ -95,3 +76,84 @@ export const pdfToImages = async (pdfBytes: Uint8Array, options: PdfToImagesOpti
return images;
};
export type PdfToImageOptions = {
scale?: number;
pageIndex: number;
/**
* The format of the image to return.
* Defaults to 'jpeg'.
*/
imageFormat?: ExportFormat;
};
export const pdfToImage = async (pdfBytes: Uint8Array, options: PdfToImageOptions) => {
const { scale = PDF_IMAGE_RENDER_SCALE, pageIndex, imageFormat = 'jpeg' } = options;
if (pageIndex !== undefined && pageIndex < 0) {
throw new Error('Page index must be greater than or equal to 0');
}
const task = await pdfjsLib.getDocument({
data: pdfBytes,
CanvasFactory: SkiaCanvasFactory,
});
const pdf = await task.promise;
const image = await getPdfPageAsImage({ pdf, pageIndex, scale, imageFormat });
void pdf.destroy().catch((e) => console.error(e));
void task.destroy().catch((e) => console.error(e));
return image;
};
type GetPdfPageAsImageOptions = {
pdf: PDFDocumentProxy;
pageIndex: number;
scale: number;
imageFormat: ExportFormat;
};
const getPdfPageAsImage = async ({
pdf,
pageIndex,
scale,
imageFormat,
}: GetPdfPageAsImageOptions) => {
const page = await pdf.getPage(pageIndex + 1);
const viewport = page.getViewport({ scale });
const canvas = new Canvas(viewport.width, viewport.height);
canvas.gpu = false;
const canvasContext = canvas.getContext('2d');
await page.render({
// @ts-expect-error napi-rs/canvas satifies the requirements
canvas,
// @ts-expect-error napi-rs/canvas satifies the requirements
canvasContext,
viewport,
}).promise;
const originalViewport = page.getViewport({ scale: 1 });
const result = {
pageIndex,
pageNumber: pageIndex + 1,
image: await canvas.toBuffer(imageFormat),
originalWidth: originalViewport.width,
originalHeight: originalViewport.height,
scale,
scaledWidth: Math.floor(viewport.width),
scaledHeight: Math.floor(viewport.height),
};
void page.cleanup();
return result;
};
@@ -5,14 +5,25 @@ import { prisma } from '@documenso/prisma';
export type CreateDocumentDataOptions = {
type: DocumentDataType;
data: string;
/**
* The initial data that was used to create the document data.
*
* If not provided, the current data will be used.
*/
initialData?: string;
};
export const createDocumentData = async ({ type, data }: CreateDocumentDataOptions) => {
export const createDocumentData = async ({
type,
data,
initialData,
}: CreateDocumentDataOptions) => {
return await prisma.documentData.create({
data: {
type,
data,
initialData: data,
initialData: initialData || data,
},
});
};
@@ -333,7 +333,7 @@ export const sendDocument = async ({
const injectFormValuesIntoDocument = async (
envelope: Envelope,
envelopeItem: Pick<EnvelopeItem, 'id'> & { documentData: DocumentData },
envelopeItem: Pick<EnvelopeItem, 'id'> & { documentData: Omit<DocumentData, 'metadata'> },
) => {
const file = await getFileServerSide(envelopeItem.documentData);
+123 -1
View File
@@ -2764,6 +2764,10 @@ msgstr "Fahre fort, indem du das Dokument ansiehst."
msgid "Continue to login"
msgstr "Weiter zum Login"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Controls how long recipients have to complete signing before the document expires. After expiration, recipients can no longer sign the document."
msgstr ""
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Controls the default email settings when new documents or templates are created"
msgstr "Kontrolliert die Standard-E-Mail-Einstellungen, wenn neue Dokumente oder Vorlagen erstellt werden."
@@ -3123,6 +3127,10 @@ msgstr "Derzeit können E-Mail-Domains nur für Plattform- und höhere Pläne ko
msgid "Custom {0} MB file"
msgstr "Benutzerdefinierte {0} MB-Datei"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Custom duration"
msgstr ""
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups._index.tsx
msgid "Custom Organisation Groups"
msgstr "Benutzerdefinierte Organisationsgruppen"
@@ -3165,6 +3173,10 @@ msgstr "Datumseinstellungen"
msgid "David is the Employee, Lucas is the Manager"
msgstr "David ist der Mitarbeiter, Lucas ist der Manager"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Days"
msgstr ""
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
#: packages/email/templates/organisation-invite.tsx
@@ -3191,6 +3203,10 @@ msgstr "Standard-E-Mail"
msgid "Default Email Settings"
msgstr "Standard-E-Mail-Einstellungen"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Default Envelope Expiration"
msgstr ""
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
msgid "Default file"
msgstr "Standarddatei"
@@ -3943,6 +3959,7 @@ msgstr "Dokumentation"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx
#: apps/remix/app/components/general/skeletons/document-edit-skeleton.tsx
#: apps/remix/app/components/general/user-profile-skeleton.tsx
#: apps/remix/app/components/general/user-profile-timur.tsx
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/tables/organisation-insights-table.tsx
@@ -4731,6 +4748,11 @@ msgstr "Alle haben unterschrieben! Sie erhalten eine Kopie des unterschriebenen
msgid "Exceeded timeout"
msgstr "Zeitüberschreitung überschritten"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "Expiration"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings._index.tsx
msgid "Expired"
msgstr "Abgelaufen"
@@ -4744,6 +4766,11 @@ msgstr "Abgelaufen"
msgid "Expires"
msgstr "Läuft ab"
#. placeholder {0}: recipient.expiresAt ? i18n.date(recipient.expiresAt, DateTime.DATETIME_MED) : 'N/A'
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Expires {0}"
msgstr ""
#. placeholder {0}: DateTime.fromMillis(Math.max(millisecondsRemaining, 0)).toFormat( 'mm:ss', )
#: apps/remix/app/components/general/document-signing/access-auth-2fa-form.tsx
msgid "Expires in {0}"
@@ -5283,6 +5310,10 @@ msgstr "Startseite (kein Ordner)"
msgid "Horizontal"
msgstr "Horizontal"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "Ich bin einverstanden, mein Konto mit dieser Organisation zu verknüpfen."
@@ -5385,6 +5416,10 @@ msgstr "Audit-Logs im Dokument einbeziehen"
msgid "Include the Signing Certificate in the Document"
msgstr "Signaturzertifikat in das Dokument einfügen"
#: apps/remix/app/components/general/billing-plans.tsx
msgid "Includes:"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-information.tsx
#: apps/remix/app/components/general/template/template-page-view-information.tsx
msgid "Information"
@@ -5406,6 +5441,7 @@ msgstr "Authentifizierungsmethode erben"
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Inherit from organisation"
@@ -5461,6 +5497,10 @@ msgstr "Ungültige Domains"
msgid "Invalid email"
msgstr "Ungültige E-Mail"
#: apps/remix/app/routes/embed+/v1+/authoring+/_layout.tsx
msgid "Invalid embedding presign token provided"
msgstr ""
#: apps/remix/app/components/general/admin-license-card.tsx
msgid "Invalid License Key"
msgstr "Ungültiger Lizenzschlüssel"
@@ -5814,6 +5854,8 @@ msgstr "Lade Vorschläge ..."
#: 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
#: packages/ui/components/pdf-viewer/pdf-viewer-konva-lazy.tsx
#: packages/ui/primitives/pdf-viewer/lazy.tsx
msgid "Loading..."
msgstr "Wird geladen..."
@@ -6120,6 +6162,10 @@ msgstr "Monatlich aktive Benutzer: Benutzer, die mindestens ein Dokument erstell
msgid "Monthly Active Users: Users that had at least one of their documents completed"
msgstr "Monatlich aktive Benutzer: Benutzer, die mindestens eines ihrer Dokumente abgeschlossen haben"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Months"
msgstr ""
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/envelopes-bulk-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
@@ -6241,6 +6287,10 @@ msgstr "Niemals"
msgid "Never expire"
msgstr "Nie ablaufen"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Never expires"
msgstr ""
#: apps/remix/app/components/forms/password.tsx
msgid "New Password"
msgstr "Neues Passwort"
@@ -6442,6 +6492,10 @@ msgstr "Keine (überschreibt globale Einstellungen)"
msgid "Not found"
msgstr "Nicht gefunden"
#: apps/remix/app/routes/embed+/_v0+/_layout.tsx
msgid "Not Found"
msgstr ""
#: apps/remix/app/components/forms/signin.tsx
msgid "Not supported"
msgstr "Nicht unterstützt"
@@ -6852,6 +6906,7 @@ msgstr "Zahlung überfällig"
msgid "PDF Document"
msgstr "PDF-Dokument"
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-status.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx
@@ -6967,6 +7022,7 @@ msgid "Please check the CSV file and make sure it is according to our format"
msgstr "Bitte prüfen Sie die CSV-Datei und stellen Sie sicher, dass sie unserem Format entspricht"
#: apps/remix/app/components/embed/embed-document-waiting-for-turn.tsx
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Please check with the parent application for more information."
msgstr "Bitte überprüfen Sie bei der übergeordneten Anwendung weitere Informationen."
@@ -7406,6 +7462,10 @@ msgstr "Der Empfänger hat das Dokument in CC gesetzt"
msgid "Recipient completed their task"
msgstr "Der Empfänger hat seine Aufgabe abgeschlossen"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Recipient expired email"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "Recipient failed to validate a 2FA token for the document"
msgstr "Dem Empfänger ist es nicht gelungen, ein 2FA-Token für das Dokument zu verifizieren"
@@ -7434,6 +7494,11 @@ msgstr "Der Empfänger hat das Dokument unterschrieben"
msgid "Recipient signing request email"
msgstr "E-Mail zur Unterzeichnungsanfrage des Empfängers"
#: packages/lib/utils/document-audit-logs.ts
msgctxt "Audit log format"
msgid "Recipient signing window expired"
msgstr ""
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Recipient updated"
msgstr "Empfänger aktualisiert"
@@ -7789,6 +7854,7 @@ msgstr "Wiederholen"
msgid "Return"
msgstr "Zurück"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
msgid "Return Home"
@@ -8209,6 +8275,10 @@ msgstr "Dokumente sofort an Empfänger senden"
msgid "Send on Behalf of Team"
msgstr "Im Namen des Teams senden"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send recipient expired email to the owner"
msgstr ""
#: apps/remix/app/components/dialogs/document-resend-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx
msgid "Send reminder"
@@ -8516,6 +8586,10 @@ msgstr "Unterzeichnungszertifikat bereitgestellt von"
msgid "Signing Complete!"
msgstr "Unterzeichnung abgeschlossen!"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "Signing Deadline Expired"
msgstr ""
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
msgid "Signing for"
msgstr "Unterzeichne für"
@@ -8540,6 +8614,26 @@ msgstr "Unterzeichnungslinks wurden für dieses Dokument erstellt."
msgid "Signing order is enabled."
msgstr "Unterzeichnungsreihenfolge ist aktiviert."
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Signing Window Expired"
msgstr ""
#. placeholder {0}: recipient.name || recipient.email
#. placeholder {1}: envelope.title
#: packages/lib/jobs/definitions/emails/send-owner-recipient-expired-email.handler.ts
msgid "Signing window expired for \"{0}\" on \"{1}\""
msgstr ""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "Signing window expired for \"{displayName}\" on \"{documentName}\""
msgstr ""
#. placeholder {0}: data.recipientName || data.recipientEmail
#: packages/lib/utils/document-audit-logs.ts
#: packages/lib/utils/document-audit-logs.ts
msgid "Signing window expired for {0}"
msgstr ""
#: apps/remix/app/components/forms/signup.tsx
msgid "Signups are disabled."
msgstr "Registrierungen sind deaktiviert."
@@ -9489,6 +9583,10 @@ msgstr "Die E-Mail des Unterzeichners"
msgid "The signer's name"
msgstr "Der Name des Unterzeichners"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "The signing deadline for this document has passed. Please contact the document owner if you need a new copy to sign."
msgstr ""
#: apps/remix/app/components/general/avatar-with-recipient.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
@@ -9496,6 +9594,14 @@ msgstr "Der Name des Unterzeichners"
msgid "The signing link has been copied to your clipboard."
msgstr "Der Signierlink wurde in die Zwischenablage kopiert."
#: packages/email/templates/recipient-expired.tsx
msgid "The signing window for \"{recipientName}\" on document \"{documentName}\" has expired."
msgstr ""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "The signing window for {displayName} on document \"{documentName}\" has expired. You can resend the document to extend their deadline or cancel the document."
msgstr ""
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
msgid "The site banner is a message that is shown at the top of the site. It can be used to display important information to your users."
msgstr "Das Seitenbanner ist eine Nachricht, die oben auf der Seite angezeigt wird. Es kann verwendet werden, um Ihren Nutzern wichtige Informationen anzuzeigen."
@@ -9861,6 +9967,10 @@ msgstr "Dies wird an alle Empfänger gesendet, sobald das Dokument vollständig
msgid "This will be sent to the document owner once the document has been fully completed."
msgstr "Dies wird an den Dokumenteneigentümer gesendet, sobald das Dokument vollständig abgeschlossen wurde."
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "This will be sent to the document owner when a recipient's signing window has expired."
msgstr ""
#: apps/remix/app/components/tables/organisation-email-domains-table.tsx
msgid "This will check and sync the status of all email domains for this organisation"
msgstr "Dadurch werden der Status aller E-Mail-Domains dieser Organisation überprüft und synchronisiert."
@@ -10792,6 +10902,7 @@ msgstr "Dokument anzeigen"
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
#: packages/email/template-components/template-document-invite.tsx
#: packages/email/template-components/template-document-rejected.tsx
#: packages/email/template-components/template-recipient-expired.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
@@ -11319,6 +11430,10 @@ msgstr "Webhook-URL"
msgid "Webhooks"
msgstr "Webhooks"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Weeks"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Welcome"
msgstr "Willkommen"
@@ -11404,6 +11519,10 @@ msgstr "Schreiben Sie eine Beschreibung, die in Ihrem öffentlichen Profil angez
msgid "Yearly"
msgstr "Jährlich"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Years"
msgstr ""
#: apps/remix/app/components/forms/branding-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
@@ -12378,6 +12497,10 @@ msgstr "Ihr Wiederherstellungscode wurde in die Zwischenablage kopiert."
msgid "Your recovery codes are listed below. Please store them in a safe place."
msgstr "Ihre Wiederherstellungscodes sind unten aufgeführt. Bitte bewahren Sie sie an einem sicheren Ort auf."
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Your signing window for this document has expired. Please contact the sender for a new invitation."
msgstr ""
#: apps/remix/app/components/forms/support-ticket-form.tsx
msgid "Your support request has been submitted. We'll get back to you soon!"
msgstr "Ihre Support-Anfrage wurde eingereicht. Wir werden uns bald bei Ihnen melden!"
@@ -12446,4 +12569,3 @@ msgstr "Ihr Verifizierungscode:"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
msgid "your-domain.com another-domain.com"
msgstr "your-domain.com another-domain.com"
+149 -26
View File
@@ -2759,6 +2759,10 @@ msgstr "Continue by viewing the document."
msgid "Continue to login"
msgstr "Continue to login"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Controls how long recipients have to complete signing before the document expires. After expiration, recipients can no longer sign the document."
msgstr "Controls how long recipients have to complete signing before the document expires. After expiration, recipients can no longer sign the document."
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Controls the default email settings when new documents or templates are created"
msgstr "Controls the default email settings when new documents or templates are created"
@@ -3118,6 +3122,10 @@ msgstr "Currently email domains can only be configured for Platform and above pl
msgid "Custom {0} MB file"
msgstr "Custom {0} MB file"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Custom duration"
msgstr "Custom duration"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups._index.tsx
msgid "Custom Organisation Groups"
msgstr "Custom Organisation Groups"
@@ -3160,6 +3168,10 @@ msgstr "Date Settings"
msgid "David is the Employee, Lucas is the Manager"
msgstr "David is the Employee, Lucas is the Manager"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Days"
msgstr "Days"
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
#: packages/email/templates/organisation-invite.tsx
@@ -3186,6 +3198,10 @@ msgstr "Default Email"
msgid "Default Email Settings"
msgstr "Default Email Settings"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Default Envelope Expiration"
msgstr "Default Envelope Expiration"
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
msgid "Default file"
msgstr "Default file"
@@ -3938,6 +3954,7 @@ msgstr "Documentation"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx
#: apps/remix/app/components/general/skeletons/document-edit-skeleton.tsx
#: apps/remix/app/components/general/user-profile-skeleton.tsx
#: apps/remix/app/components/general/user-profile-timur.tsx
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/tables/organisation-insights-table.tsx
@@ -4298,6 +4315,22 @@ msgstr "Email Preferences"
msgid "Email preferences updated"
msgstr "Email preferences updated"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Email recipients when a pending document is deleted"
msgstr "Email recipients when a pending document is deleted"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Email recipients when the document is completed"
msgstr "Email recipients when the document is completed"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Email recipients when they're removed from a pending document"
msgstr "Email recipients when they're removed from a pending document"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Email recipients with a signing request"
msgstr "Email recipients with a signing request"
#: packages/lib/utils/document-audit-logs.ts
msgctxt "Audit log format"
msgid "Email resent"
@@ -4323,6 +4356,18 @@ msgstr "Email sent!"
msgid "Email Settings"
msgstr "Email Settings"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Email the owner when a recipient signs"
msgstr "Email the owner when a recipient signs"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Email the owner when the document is completed"
msgstr "Email the owner when the document is completed"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Email the signer if the document is still pending"
msgstr "Email the signer if the document is still pending"
#: apps/remix/app/components/general/document-signing/access-auth-2fa-form.tsx
msgid "Email verification"
msgstr "Email verification"
@@ -4698,6 +4743,11 @@ msgstr "Everyone has signed! You will receive an email copy of the signed docume
msgid "Exceeded timeout"
msgstr "Exceeded timeout"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "Expiration"
msgstr "Expiration"
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings._index.tsx
msgid "Expired"
msgstr "Expired"
@@ -4711,6 +4761,11 @@ msgstr "Expired"
msgid "Expires"
msgstr "Expires"
#. placeholder {0}: recipient.expiresAt ? i18n.date(recipient.expiresAt, DateTime.DATETIME_MED) : 'N/A'
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Expires {0}"
msgstr "Expires {0}"
#. placeholder {0}: DateTime.fromMillis(Math.max(millisecondsRemaining, 0)).toFormat( 'mm:ss', )
#: apps/remix/app/components/general/document-signing/access-auth-2fa-form.tsx
msgid "Expires in {0}"
@@ -5250,6 +5305,10 @@ msgstr "Home (No Folder)"
msgid "Horizontal"
msgstr "Horizontal"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "I agree to link my account with this organization"
@@ -5352,6 +5411,10 @@ msgstr "Include the Audit Logs in the Document"
msgid "Include the Signing Certificate in the Document"
msgstr "Include the Signing Certificate in the Document"
#: apps/remix/app/components/general/billing-plans.tsx
msgid "Includes:"
msgstr "Includes:"
#: apps/remix/app/components/general/document/document-page-view-information.tsx
#: apps/remix/app/components/general/template/template-page-view-information.tsx
msgid "Information"
@@ -5373,6 +5436,7 @@ msgstr "Inherit authentication method"
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Inherit from organisation"
@@ -5428,6 +5492,10 @@ msgstr "Invalid domains"
msgid "Invalid email"
msgstr "Invalid email"
#: apps/remix/app/routes/embed+/v1+/authoring+/_layout.tsx
msgid "Invalid embedding presign token provided"
msgstr "Invalid embedding presign token provided"
#: apps/remix/app/components/general/admin-license-card.tsx
msgid "Invalid License Key"
msgstr "Invalid License Key"
@@ -5781,6 +5849,8 @@ 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
#: packages/ui/components/pdf-viewer/pdf-viewer-konva-lazy.tsx
#: packages/ui/primitives/pdf-viewer/lazy.tsx
msgid "Loading..."
msgstr "Loading..."
@@ -6087,6 +6157,10 @@ msgstr "Monthly Active Users: Users that created at least one Document"
msgid "Monthly Active Users: Users that had at least one of their documents completed"
msgstr "Monthly Active Users: Users that had at least one of their documents completed"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Months"
msgstr "Months"
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/envelopes-bulk-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
@@ -6208,6 +6282,10 @@ msgstr "Never"
msgid "Never expire"
msgstr "Never expire"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Never expires"
msgstr "Never expires"
#: apps/remix/app/components/forms/password.tsx
msgid "New Password"
msgstr "New Password"
@@ -6409,6 +6487,10 @@ msgstr "None (Overrides global settings)"
msgid "Not found"
msgstr "Not found"
#: apps/remix/app/routes/embed+/_v0+/_layout.tsx
msgid "Not Found"
msgstr "Not Found"
#: apps/remix/app/components/forms/signin.tsx
msgid "Not supported"
msgstr "Not supported"
@@ -6819,6 +6901,7 @@ msgstr "Payment overdue"
msgid "PDF Document"
msgstr "PDF Document"
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-status.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx
@@ -6934,6 +7017,7 @@ msgid "Please check the CSV file and make sure it is according to our format"
msgstr "Please check the CSV file and make sure it is according to our format"
#: apps/remix/app/components/embed/embed-document-waiting-for-turn.tsx
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Please check with the parent application for more information."
msgstr "Please check with the parent application for more information."
@@ -7373,6 +7457,10 @@ msgstr "Recipient CC'd the document"
msgid "Recipient completed their task"
msgstr "Recipient completed their task"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Recipient expired email"
msgstr "Recipient expired email"
#: packages/lib/utils/document-audit-logs.ts
msgid "Recipient failed to validate a 2FA token for the document"
msgstr "Recipient failed to validate a 2FA token for the document"
@@ -7401,6 +7489,11 @@ msgstr "Recipient signed the document"
msgid "Recipient signing request email"
msgstr "Recipient signing request email"
#: packages/lib/utils/document-audit-logs.ts
msgctxt "Audit log format"
msgid "Recipient signing window expired"
msgstr "Recipient signing window expired"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Recipient updated"
msgstr "Recipient updated"
@@ -7756,6 +7849,7 @@ msgstr "Retry"
msgid "Return"
msgstr "Return"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
msgid "Return Home"
@@ -8164,22 +8258,6 @@ msgstr "Send document"
msgid "Send Document"
msgstr "Send Document"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send document completed email"
msgstr "Send document completed email"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send document completed email to the owner"
msgstr "Send document completed email to the owner"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send document deleted email"
msgstr "Send document deleted email"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send document pending email"
msgstr "Send document pending email"
#: packages/email/templates/confirm-team-email.tsx
msgid "Send documents on behalf of the team using the email address"
msgstr "Send documents on behalf of the team using the email address"
@@ -8193,16 +8271,8 @@ msgid "Send on Behalf of Team"
msgstr "Send on Behalf of Team"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send recipient removed email"
msgstr "Send recipient removed email"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send recipient signed email"
msgstr "Send recipient signed email"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send recipient signing request email"
msgstr "Send recipient signing request email"
msgid "Send recipient expired email to the owner"
msgstr "Send recipient expired email to the owner"
#: apps/remix/app/components/dialogs/document-resend-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx
@@ -8511,6 +8581,10 @@ msgstr "Signing certificate provided by"
msgid "Signing Complete!"
msgstr "Signing Complete!"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "Signing Deadline Expired"
msgstr "Signing Deadline Expired"
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
msgid "Signing for"
msgstr "Signing for"
@@ -8535,6 +8609,26 @@ msgstr "Signing links have been generated for this document."
msgid "Signing order is enabled."
msgstr "Signing order is enabled."
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Signing Window Expired"
msgstr "Signing Window Expired"
#. placeholder {0}: recipient.name || recipient.email
#. placeholder {1}: envelope.title
#: packages/lib/jobs/definitions/emails/send-owner-recipient-expired-email.handler.ts
msgid "Signing window expired for \"{0}\" on \"{1}\""
msgstr "Signing window expired for \"{0}\" on \"{1}\""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "Signing window expired for \"{displayName}\" on \"{documentName}\""
msgstr "Signing window expired for \"{displayName}\" on \"{documentName}\""
#. placeholder {0}: data.recipientName || data.recipientEmail
#: packages/lib/utils/document-audit-logs.ts
#: packages/lib/utils/document-audit-logs.ts
msgid "Signing window expired for {0}"
msgstr "Signing window expired for {0}"
#: apps/remix/app/components/forms/signup.tsx
msgid "Signups are disabled."
msgstr "Signups are disabled."
@@ -9484,6 +9578,10 @@ msgstr "The signer's email"
msgid "The signer's name"
msgstr "The signer's name"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "The signing deadline for this document has passed. Please contact the document owner if you need a new copy to sign."
msgstr "The signing deadline for this document has passed. Please contact the document owner if you need a new copy to sign."
#: apps/remix/app/components/general/avatar-with-recipient.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
@@ -9491,6 +9589,14 @@ msgstr "The signer's name"
msgid "The signing link has been copied to your clipboard."
msgstr "The signing link has been copied to your clipboard."
#: packages/email/templates/recipient-expired.tsx
msgid "The signing window for \"{recipientName}\" on document \"{documentName}\" has expired."
msgstr "The signing window for \"{recipientName}\" on document \"{documentName}\" has expired."
#: packages/email/template-components/template-recipient-expired.tsx
msgid "The signing window for {displayName} on document \"{documentName}\" has expired. You can resend the document to extend their deadline or cancel the document."
msgstr "The signing window for {displayName} on document \"{documentName}\" has expired. You can resend the document to extend their deadline or cancel the document."
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
msgid "The site banner is a message that is shown at the top of the site. It can be used to display important information to your users."
msgstr "The site banner is a message that is shown at the top of the site. It can be used to display important information to your users."
@@ -9856,6 +9962,10 @@ msgstr "This will be sent to all recipients once the document has been fully com
msgid "This will be sent to the document owner once the document has been fully completed."
msgstr "This will be sent to the document owner once the document has been fully completed."
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "This will be sent to the document owner when a recipient's signing window has expired."
msgstr "This will be sent to the document owner when a recipient's signing window has expired."
#: apps/remix/app/components/tables/organisation-email-domains-table.tsx
msgid "This will check and sync the status of all email domains for this organisation"
msgstr "This will check and sync the status of all email domains for this organisation"
@@ -10787,6 +10897,7 @@ msgstr "View document"
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
#: packages/email/template-components/template-document-invite.tsx
#: packages/email/template-components/template-document-rejected.tsx
#: packages/email/template-components/template-recipient-expired.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
@@ -11314,6 +11425,10 @@ msgstr "Webhook URL"
msgid "Webhooks"
msgstr "Webhooks"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Weeks"
msgstr "Weeks"
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Welcome"
msgstr "Welcome"
@@ -11399,6 +11514,10 @@ msgstr "Write a description to display on your public profile"
msgid "Yearly"
msgstr "Yearly"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Years"
msgstr "Years"
#: apps/remix/app/components/forms/branding-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
@@ -12373,6 +12492,10 @@ msgstr "Your recovery code has been copied to your clipboard."
msgid "Your recovery codes are listed below. Please store them in a safe place."
msgstr "Your recovery codes are listed below. Please store them in a safe place."
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Your signing window for this document has expired. Please contact the sender for a new invitation."
msgstr "Your signing window for this document has expired. Please contact the sender for a new invitation."
#: apps/remix/app/components/forms/support-ticket-form.tsx
msgid "Your support request has been submitted. We'll get back to you soon!"
msgstr "Your support request has been submitted. We'll get back to you soon!"
+123 -1
View File
@@ -2764,6 +2764,10 @@ msgstr "Continúa viendo el documento."
msgid "Continue to login"
msgstr "Continuar con el inicio de sesión"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Controls how long recipients have to complete signing before the document expires. After expiration, recipients can no longer sign the document."
msgstr ""
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Controls the default email settings when new documents or templates are created"
msgstr "Controla la configuración de correo electrónico predeterminada cuando se crean nuevos documentos o plantillas"
@@ -3123,6 +3127,10 @@ msgstr "Actualmente los dominios de correo electrónico solo se pueden configura
msgid "Custom {0} MB file"
msgstr "Archivo personalizado de {0} MB"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Custom duration"
msgstr ""
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups._index.tsx
msgid "Custom Organisation Groups"
msgstr "Grupos de Organización Personalizados"
@@ -3165,6 +3173,10 @@ msgstr "Configuración de Fecha"
msgid "David is the Employee, Lucas is the Manager"
msgstr "David es el empleado, Lucas es el gerente"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Days"
msgstr ""
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
#: packages/email/templates/organisation-invite.tsx
@@ -3191,6 +3203,10 @@ msgstr "Correo predeterminado"
msgid "Default Email Settings"
msgstr "Configuración de correo predeterminada"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Default Envelope Expiration"
msgstr ""
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
msgid "Default file"
msgstr "Archivo por defecto"
@@ -3943,6 +3959,7 @@ msgstr "Documentación"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx
#: apps/remix/app/components/general/skeletons/document-edit-skeleton.tsx
#: apps/remix/app/components/general/user-profile-skeleton.tsx
#: apps/remix/app/components/general/user-profile-timur.tsx
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/tables/organisation-insights-table.tsx
@@ -4731,6 +4748,11 @@ msgstr "¡Todos han firmado! Recibirás una copia del documento firmado por corr
msgid "Exceeded timeout"
msgstr "Tiempo de espera excedido"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "Expiration"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings._index.tsx
msgid "Expired"
msgstr "Expirado"
@@ -4744,6 +4766,11 @@ msgstr "Vencida"
msgid "Expires"
msgstr "Vence"
#. placeholder {0}: recipient.expiresAt ? i18n.date(recipient.expiresAt, DateTime.DATETIME_MED) : 'N/A'
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Expires {0}"
msgstr ""
#. placeholder {0}: DateTime.fromMillis(Math.max(millisecondsRemaining, 0)).toFormat( 'mm:ss', )
#: apps/remix/app/components/general/document-signing/access-auth-2fa-form.tsx
msgid "Expires in {0}"
@@ -5283,6 +5310,10 @@ msgstr "Inicio (Sin Carpeta)"
msgid "Horizontal"
msgstr "Horizontal"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "Acepto vincular mi cuenta con esta organización"
@@ -5385,6 +5416,10 @@ msgstr "Incluir los registros de auditoría en el documento"
msgid "Include the Signing Certificate in the Document"
msgstr "Incluir el certificado de firma en el documento"
#: apps/remix/app/components/general/billing-plans.tsx
msgid "Includes:"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-information.tsx
#: apps/remix/app/components/general/template/template-page-view-information.tsx
msgid "Information"
@@ -5406,6 +5441,7 @@ msgstr "Heredar método de autenticación"
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Inherit from organisation"
@@ -5461,6 +5497,10 @@ msgstr "Dominios no válidos"
msgid "Invalid email"
msgstr "Email inválido"
#: apps/remix/app/routes/embed+/v1+/authoring+/_layout.tsx
msgid "Invalid embedding presign token provided"
msgstr ""
#: apps/remix/app/components/general/admin-license-card.tsx
msgid "Invalid License Key"
msgstr "Clave de licencia no válida"
@@ -5814,6 +5854,8 @@ msgstr "Cargando sugerencias..."
#: 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
#: packages/ui/components/pdf-viewer/pdf-viewer-konva-lazy.tsx
#: packages/ui/primitives/pdf-viewer/lazy.tsx
msgid "Loading..."
msgstr "Cargando..."
@@ -6120,6 +6162,10 @@ msgstr "Usuarios activos mensuales: Usuarios que crearon al menos un documento"
msgid "Monthly Active Users: Users that had at least one of their documents completed"
msgstr "Usuarios activos mensuales: Usuarios que completaron al menos uno de sus documentos"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Months"
msgstr ""
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/envelopes-bulk-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
@@ -6241,6 +6287,10 @@ msgstr "Nunca"
msgid "Never expire"
msgstr "Nunca expira"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Never expires"
msgstr ""
#: apps/remix/app/components/forms/password.tsx
msgid "New Password"
msgstr "Nueva Contraseña"
@@ -6442,6 +6492,10 @@ msgstr "Ninguno (anula la configuración global)"
msgid "Not found"
msgstr "No Encontrado"
#: apps/remix/app/routes/embed+/_v0+/_layout.tsx
msgid "Not Found"
msgstr ""
#: apps/remix/app/components/forms/signin.tsx
msgid "Not supported"
msgstr "No soportado"
@@ -6852,6 +6906,7 @@ msgstr "Pago atrasado"
msgid "PDF Document"
msgstr "Documento PDF"
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-status.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx
@@ -6967,6 +7022,7 @@ msgid "Please check the CSV file and make sure it is according to our format"
msgstr "Por favor, revisa el archivo CSV y asegúrate de que esté de acuerdo con nuestro formato"
#: apps/remix/app/components/embed/embed-document-waiting-for-turn.tsx
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Please check with the parent application for more information."
msgstr "Por favor consulta con la aplicación principal para obtener más información."
@@ -7406,6 +7462,10 @@ msgstr "El destinatario envió una copia del documento"
msgid "Recipient completed their task"
msgstr "El destinatario completó su tarea"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Recipient expired email"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "Recipient failed to validate a 2FA token for the document"
msgstr "El destinatario no pudo validar un token 2FA para el documento"
@@ -7434,6 +7494,11 @@ msgstr "El destinatario firmó el documento"
msgid "Recipient signing request email"
msgstr "Correo electrónico de solicitud de firma de destinatario"
#: packages/lib/utils/document-audit-logs.ts
msgctxt "Audit log format"
msgid "Recipient signing window expired"
msgstr ""
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Recipient updated"
msgstr "Destinatario actualizado"
@@ -7789,6 +7854,7 @@ msgstr "Reintentar"
msgid "Return"
msgstr "Regresar"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
msgid "Return Home"
@@ -8209,6 +8275,10 @@ msgstr "Enviar documentos a los destinatarios inmediatamente"
msgid "Send on Behalf of Team"
msgstr "Enviar en nombre del equipo"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send recipient expired email to the owner"
msgstr ""
#: apps/remix/app/components/dialogs/document-resend-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx
msgid "Send reminder"
@@ -8516,6 +8586,10 @@ msgstr "Certificado de firma proporcionado por"
msgid "Signing Complete!"
msgstr "¡Firma completa!"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "Signing Deadline Expired"
msgstr ""
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
msgid "Signing for"
msgstr "Firmando para"
@@ -8540,6 +8614,26 @@ msgstr "Se han generado enlaces de firma para este documento."
msgid "Signing order is enabled."
msgstr "El orden de firma está habilitado."
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Signing Window Expired"
msgstr ""
#. placeholder {0}: recipient.name || recipient.email
#. placeholder {1}: envelope.title
#: packages/lib/jobs/definitions/emails/send-owner-recipient-expired-email.handler.ts
msgid "Signing window expired for \"{0}\" on \"{1}\""
msgstr ""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "Signing window expired for \"{displayName}\" on \"{documentName}\""
msgstr ""
#. placeholder {0}: data.recipientName || data.recipientEmail
#: packages/lib/utils/document-audit-logs.ts
#: packages/lib/utils/document-audit-logs.ts
msgid "Signing window expired for {0}"
msgstr ""
#: apps/remix/app/components/forms/signup.tsx
msgid "Signups are disabled."
msgstr "Las inscripciones están deshabilitadas."
@@ -9489,6 +9583,10 @@ msgstr "El correo electrónico del firmante"
msgid "The signer's name"
msgstr "El nombre del firmante"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "The signing deadline for this document has passed. Please contact the document owner if you need a new copy to sign."
msgstr ""
#: apps/remix/app/components/general/avatar-with-recipient.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
@@ -9496,6 +9594,14 @@ msgstr "El nombre del firmante"
msgid "The signing link has been copied to your clipboard."
msgstr "El enlace de firma ha sido copiado a tu portapapeles."
#: packages/email/templates/recipient-expired.tsx
msgid "The signing window for \"{recipientName}\" on document \"{documentName}\" has expired."
msgstr ""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "The signing window for {displayName} on document \"{documentName}\" has expired. You can resend the document to extend their deadline or cancel the document."
msgstr ""
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
msgid "The site banner is a message that is shown at the top of the site. It can be used to display important information to your users."
msgstr "El banner del sitio es un mensaje que se muestra en la parte superior del sitio. Se puede usar para mostrar información importante a tus usuarios."
@@ -9861,6 +9967,10 @@ msgstr "Esto se enviará a todos los destinatarios una vez que el documento est
msgid "This will be sent to the document owner once the document has been fully completed."
msgstr "Esto se enviará al propietario del documento una vez que el documento se haya completado por completo."
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "This will be sent to the document owner when a recipient's signing window has expired."
msgstr ""
#: apps/remix/app/components/tables/organisation-email-domains-table.tsx
msgid "This will check and sync the status of all email domains for this organisation"
msgstr "Esto verificará y sincronizará el estado de todos los dominios de correo electrónico para esta organización."
@@ -10792,6 +10902,7 @@ msgstr "Ver documento"
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
#: packages/email/template-components/template-document-invite.tsx
#: packages/email/template-components/template-document-rejected.tsx
#: packages/email/template-components/template-recipient-expired.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
@@ -11319,6 +11430,10 @@ msgstr "URL del Webhook"
msgid "Webhooks"
msgstr "Webhooks"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Weeks"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Welcome"
msgstr "Bienvenido"
@@ -11404,6 +11519,10 @@ msgstr "Escribe una descripción para mostrar en tu perfil público"
msgid "Yearly"
msgstr "Anual"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Years"
msgstr ""
#: apps/remix/app/components/forms/branding-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
@@ -12378,6 +12497,10 @@ msgstr "Tu código de recuperación ha sido copiado en tu portapapeles."
msgid "Your recovery codes are listed below. Please store them in a safe place."
msgstr "Tus códigos de recuperación se enumeran a continuación. Por favor, guárdalos en un lugar seguro."
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Your signing window for this document has expired. Please contact the sender for a new invitation."
msgstr ""
#: apps/remix/app/components/forms/support-ticket-form.tsx
msgid "Your support request has been submitted. We'll get back to you soon!"
msgstr "Su solicitud de soporte ha sido enviada. ¡Nos pondremos en contacto contigo pronto!"
@@ -12446,4 +12569,3 @@ msgstr "Su código de verificación:"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
msgid "your-domain.com another-domain.com"
msgstr "su-dominio.com otro-dominio.com"
+123 -1
View File
@@ -2764,6 +2764,10 @@ msgstr "Continuer en consultant le document."
msgid "Continue to login"
msgstr "Continuer vers la connexion"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Controls how long recipients have to complete signing before the document expires. After expiration, recipients can no longer sign the document."
msgstr ""
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Controls the default email settings when new documents or templates are created"
msgstr "Contrôler les paramètres de messagerie par défaut lors de la création de nouveaux documents ou modèles"
@@ -3123,6 +3127,10 @@ msgstr "Actuellement, les domaines de messagerie ne peuvent être configurés qu
msgid "Custom {0} MB file"
msgstr "Fichier personnalisé de {0} Mo"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Custom duration"
msgstr ""
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups._index.tsx
msgid "Custom Organisation Groups"
msgstr "Groupes d'organisation personnalisés"
@@ -3165,6 +3173,10 @@ msgstr "Paramètres de la date"
msgid "David is the Employee, Lucas is the Manager"
msgstr "David est l'employé, Lucas est le manager"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Days"
msgstr ""
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
#: packages/email/templates/organisation-invite.tsx
@@ -3191,6 +3203,10 @@ msgstr "E-mail par défaut"
msgid "Default Email Settings"
msgstr "Paramètres d'e-mail par défaut"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Default Envelope Expiration"
msgstr ""
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
msgid "Default file"
msgstr "Fichier par défaut"
@@ -3943,6 +3959,7 @@ msgstr "Documentation"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx
#: apps/remix/app/components/general/skeletons/document-edit-skeleton.tsx
#: apps/remix/app/components/general/user-profile-skeleton.tsx
#: apps/remix/app/components/general/user-profile-timur.tsx
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/tables/organisation-insights-table.tsx
@@ -4731,6 +4748,11 @@ msgstr "Tout le monde a signé ! Vous recevrez une copie du document signé par
msgid "Exceeded timeout"
msgstr "Délai dépassé"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "Expiration"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings._index.tsx
msgid "Expired"
msgstr "Expiré"
@@ -4744,6 +4766,11 @@ msgstr "Expiré"
msgid "Expires"
msgstr "Expire le"
#. placeholder {0}: recipient.expiresAt ? i18n.date(recipient.expiresAt, DateTime.DATETIME_MED) : 'N/A'
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Expires {0}"
msgstr ""
#. placeholder {0}: DateTime.fromMillis(Math.max(millisecondsRemaining, 0)).toFormat( 'mm:ss', )
#: apps/remix/app/components/general/document-signing/access-auth-2fa-form.tsx
msgid "Expires in {0}"
@@ -5283,6 +5310,10 @@ msgstr "Accueil (Pas de Dossier)"
msgid "Horizontal"
msgstr "Horizontal"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "J'accepte de lier mon compte avec cette organisation"
@@ -5385,6 +5416,10 @@ msgstr "Inclure les journaux d'audit dans le document"
msgid "Include the Signing Certificate in the Document"
msgstr "Includez le certificat de signature dans le document"
#: apps/remix/app/components/general/billing-plans.tsx
msgid "Includes:"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-information.tsx
#: apps/remix/app/components/general/template/template-page-view-information.tsx
msgid "Information"
@@ -5406,6 +5441,7 @@ msgstr "Hériter de la méthode d'authentification"
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Inherit from organisation"
@@ -5461,6 +5497,10 @@ msgstr "Domaines invalides"
msgid "Invalid email"
msgstr "Email invalide"
#: apps/remix/app/routes/embed+/v1+/authoring+/_layout.tsx
msgid "Invalid embedding presign token provided"
msgstr ""
#: apps/remix/app/components/general/admin-license-card.tsx
msgid "Invalid License Key"
msgstr "Clé de licence invalide"
@@ -5814,6 +5854,8 @@ msgstr "Chargement des 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
#: packages/ui/components/pdf-viewer/pdf-viewer-konva-lazy.tsx
#: packages/ui/primitives/pdf-viewer/lazy.tsx
msgid "Loading..."
msgstr "Chargement..."
@@ -6120,6 +6162,10 @@ msgstr "Utilisateurs actifs mensuels : utilisateurs ayant créé au moins un doc
msgid "Monthly Active Users: Users that had at least one of their documents completed"
msgstr "Utilisateurs actifs mensuels : utilisateurs ayant terminé au moins un de leurs documents"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Months"
msgstr ""
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/envelopes-bulk-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
@@ -6241,6 +6287,10 @@ msgstr "Jamais"
msgid "Never expire"
msgstr "Ne jamais expirer"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Never expires"
msgstr ""
#: apps/remix/app/components/forms/password.tsx
msgid "New Password"
msgstr "Nouveau Mot de Passe"
@@ -6442,6 +6492,10 @@ msgstr "Aucun (remplace les paramètres globaux)"
msgid "Not found"
msgstr "Non trouvé"
#: apps/remix/app/routes/embed+/_v0+/_layout.tsx
msgid "Not Found"
msgstr ""
#: apps/remix/app/components/forms/signin.tsx
msgid "Not supported"
msgstr "Non pris en charge"
@@ -6852,6 +6906,7 @@ msgstr "Paiement en retard"
msgid "PDF Document"
msgstr "Document PDF"
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-status.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx
@@ -6967,6 +7022,7 @@ msgid "Please check the CSV file and make sure it is according to our format"
msgstr "Veuillez vérifier le fichier CSV et vous assurer qu'il est conforme à notre format"
#: apps/remix/app/components/embed/embed-document-waiting-for-turn.tsx
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Please check with the parent application for more information."
msgstr "Veuillez vérifier auprès de l'application parent pour plus d'informations."
@@ -7406,6 +7462,10 @@ msgstr "Le destinataire a mis le document en copie"
msgid "Recipient completed their task"
msgstr "Le destinataire a terminé sa tâche"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Recipient expired email"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "Recipient failed to validate a 2FA token for the document"
msgstr "Le destinataire n'a pas réussi à valider un jeton 2FA pour le document"
@@ -7434,6 +7494,11 @@ msgstr "Le destinataire a signé le document"
msgid "Recipient signing request email"
msgstr "E-mail de demande de signature de destinataire"
#: packages/lib/utils/document-audit-logs.ts
msgctxt "Audit log format"
msgid "Recipient signing window expired"
msgstr ""
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Recipient updated"
msgstr "Destinataire mis à jour"
@@ -7789,6 +7854,7 @@ msgstr "Réessayer"
msgid "Return"
msgstr "Retour"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
msgid "Return Home"
@@ -8209,6 +8275,10 @@ msgstr "Envoyer les documents aux destinataires immédiatement"
msgid "Send on Behalf of Team"
msgstr "Envoyer au nom de l'équipe"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send recipient expired email to the owner"
msgstr ""
#: apps/remix/app/components/dialogs/document-resend-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx
msgid "Send reminder"
@@ -8516,6 +8586,10 @@ msgstr "Certificat de signature fourni par"
msgid "Signing Complete!"
msgstr "Signature Complète !"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "Signing Deadline Expired"
msgstr ""
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
msgid "Signing for"
msgstr "Signé pour"
@@ -8540,6 +8614,26 @@ msgstr "Des liens de signature ont été générés pour ce document."
msgid "Signing order is enabled."
msgstr "L'ordre de signature est activé."
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Signing Window Expired"
msgstr ""
#. placeholder {0}: recipient.name || recipient.email
#. placeholder {1}: envelope.title
#: packages/lib/jobs/definitions/emails/send-owner-recipient-expired-email.handler.ts
msgid "Signing window expired for \"{0}\" on \"{1}\""
msgstr ""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "Signing window expired for \"{displayName}\" on \"{documentName}\""
msgstr ""
#. placeholder {0}: data.recipientName || data.recipientEmail
#: packages/lib/utils/document-audit-logs.ts
#: packages/lib/utils/document-audit-logs.ts
msgid "Signing window expired for {0}"
msgstr ""
#: apps/remix/app/components/forms/signup.tsx
msgid "Signups are disabled."
msgstr "Les inscriptions sont désactivées."
@@ -9489,6 +9583,10 @@ msgstr "L'email du signataire"
msgid "The signer's name"
msgstr "Le nom du signataire"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "The signing deadline for this document has passed. Please contact the document owner if you need a new copy to sign."
msgstr ""
#: apps/remix/app/components/general/avatar-with-recipient.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
@@ -9496,6 +9594,14 @@ msgstr "Le nom du signataire"
msgid "The signing link has been copied to your clipboard."
msgstr "Le lien de signature a été copié dans votre presse-papiers."
#: packages/email/templates/recipient-expired.tsx
msgid "The signing window for \"{recipientName}\" on document \"{documentName}\" has expired."
msgstr ""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "The signing window for {displayName} on document \"{documentName}\" has expired. You can resend the document to extend their deadline or cancel the document."
msgstr ""
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
msgid "The site banner is a message that is shown at the top of the site. It can be used to display important information to your users."
msgstr "La bannière du site est un message affiché en haut du site. Elle peut être utilisée pour afficher des informations importantes à vos utilisateurs."
@@ -9861,6 +9967,10 @@ msgstr "Cela sera envoyé à tous les destinataires une fois que le document aur
msgid "This will be sent to the document owner once the document has been fully completed."
msgstr "Cela sera envoyé au propriétaire du document une fois que le document aura été entièrement complété."
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "This will be sent to the document owner when a recipient's signing window has expired."
msgstr ""
#: apps/remix/app/components/tables/organisation-email-domains-table.tsx
msgid "This will check and sync the status of all email domains for this organisation"
msgstr "Cela vérifiera et synchronisera l'état de tous les domaines de messagerie de cette organisation"
@@ -10792,6 +10902,7 @@ msgstr "Voir le document"
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
#: packages/email/template-components/template-document-invite.tsx
#: packages/email/template-components/template-document-rejected.tsx
#: packages/email/template-components/template-recipient-expired.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
@@ -11319,6 +11430,10 @@ msgstr "URL du webhook"
msgid "Webhooks"
msgstr "Webhooks"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Weeks"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Welcome"
msgstr "Bienvenue"
@@ -11404,6 +11519,10 @@ msgstr "Écrivez une description à afficher sur votre profil public"
msgid "Yearly"
msgstr "Annuel"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Years"
msgstr ""
#: apps/remix/app/components/forms/branding-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
@@ -12378,6 +12497,10 @@ msgstr "Votre code de récupération a été copié dans votre presse-papiers."
msgid "Your recovery codes are listed below. Please store them in a safe place."
msgstr "Vos codes de récupération sont listés ci-dessous. Veuillez les conserver dans un endroit sûr."
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Your signing window for this document has expired. Please contact the sender for a new invitation."
msgstr ""
#: apps/remix/app/components/forms/support-ticket-form.tsx
msgid "Your support request has been submitted. We'll get back to you soon!"
msgstr "Votre demande de support a été soumise. Nous vous recontacterons bientôt !"
@@ -12446,4 +12569,3 @@ msgstr "Votre code de vérification :"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
msgid "your-domain.com another-domain.com"
msgstr "your-domain.com another-domain.com"
+123 -1
View File
@@ -2764,6 +2764,10 @@ msgstr "Continua visualizzando il documento."
msgid "Continue to login"
msgstr "Continua per accedere"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Controls how long recipients have to complete signing before the document expires. After expiration, recipients can no longer sign the document."
msgstr ""
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Controls the default email settings when new documents or templates are created"
msgstr "Controlla le impostazioni email predefinite quando vengono creati nuovi documenti o modelli"
@@ -3123,6 +3127,10 @@ msgstr "Attualmente i domini email possono essere configurati solo per i piani P
msgid "Custom {0} MB file"
msgstr "File personalizzato da {0} MB"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Custom duration"
msgstr ""
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups._index.tsx
msgid "Custom Organisation Groups"
msgstr "Gruppi di Organizzazione Personalizzati"
@@ -3165,6 +3173,10 @@ msgstr "Impostazioni della data"
msgid "David is the Employee, Lucas is the Manager"
msgstr "David è il dipendente, Lucas è il manager"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Days"
msgstr ""
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
#: packages/email/templates/organisation-invite.tsx
@@ -3191,6 +3203,10 @@ msgstr "Email Predefinita"
msgid "Default Email Settings"
msgstr "Impostazioni Email Predefinite"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Default Envelope Expiration"
msgstr ""
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
msgid "Default file"
msgstr "File predefinito"
@@ -3943,6 +3959,7 @@ msgstr "Documentazione"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx
#: apps/remix/app/components/general/skeletons/document-edit-skeleton.tsx
#: apps/remix/app/components/general/user-profile-skeleton.tsx
#: apps/remix/app/components/general/user-profile-timur.tsx
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/tables/organisation-insights-table.tsx
@@ -4731,6 +4748,11 @@ msgstr "Tutti hanno firmato! Riceverai una copia del documento firmato via email
msgid "Exceeded timeout"
msgstr "Tempo scaduto"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "Expiration"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings._index.tsx
msgid "Expired"
msgstr "Scaduto"
@@ -4744,6 +4766,11 @@ msgstr "Scaduto"
msgid "Expires"
msgstr "Scade"
#. placeholder {0}: recipient.expiresAt ? i18n.date(recipient.expiresAt, DateTime.DATETIME_MED) : 'N/A'
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Expires {0}"
msgstr ""
#. placeholder {0}: DateTime.fromMillis(Math.max(millisecondsRemaining, 0)).toFormat( 'mm:ss', )
#: apps/remix/app/components/general/document-signing/access-auth-2fa-form.tsx
msgid "Expires in {0}"
@@ -5283,6 +5310,10 @@ msgstr "Home (Nessuna Cartella)"
msgid "Horizontal"
msgstr "Orizzontale"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "Accetto di collegare il mio account con questa organizzazione"
@@ -5385,6 +5416,10 @@ msgstr "Includi i Registri di Audit nel Documento"
msgid "Include the Signing Certificate in the Document"
msgstr "Includi il certificato di firma nel documento"
#: apps/remix/app/components/general/billing-plans.tsx
msgid "Includes:"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-information.tsx
#: apps/remix/app/components/general/template/template-page-view-information.tsx
msgid "Information"
@@ -5406,6 +5441,7 @@ msgstr "Ereditare metodo di autenticazione"
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Inherit from organisation"
@@ -5461,6 +5497,10 @@ msgstr "Domini non validi"
msgid "Invalid email"
msgstr "Email non valida"
#: apps/remix/app/routes/embed+/v1+/authoring+/_layout.tsx
msgid "Invalid embedding presign token provided"
msgstr ""
#: apps/remix/app/components/general/admin-license-card.tsx
msgid "Invalid License Key"
msgstr "Chiave di licenza non valida"
@@ -5814,6 +5854,8 @@ msgstr "Caricamento suggerimenti..."
#: 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
#: packages/ui/components/pdf-viewer/pdf-viewer-konva-lazy.tsx
#: packages/ui/primitives/pdf-viewer/lazy.tsx
msgid "Loading..."
msgstr "Caricamento in corso..."
@@ -6120,6 +6162,10 @@ msgstr "Utenti attivi mensili: Utenti che hanno creato almeno un documento"
msgid "Monthly Active Users: Users that had at least one of their documents completed"
msgstr "Utenti attivi mensili: Utenti con almeno uno dei loro documenti completati"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Months"
msgstr ""
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/envelopes-bulk-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
@@ -6241,6 +6287,10 @@ msgstr "Mai"
msgid "Never expire"
msgstr "Mai scadere"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Never expires"
msgstr ""
#: apps/remix/app/components/forms/password.tsx
msgid "New Password"
msgstr "Nuova Password"
@@ -6442,6 +6492,10 @@ msgstr "Nessuno (sovrascrive le impostazioni globali)"
msgid "Not found"
msgstr "Non trovato"
#: apps/remix/app/routes/embed+/_v0+/_layout.tsx
msgid "Not Found"
msgstr ""
#: apps/remix/app/components/forms/signin.tsx
msgid "Not supported"
msgstr "Non supportato"
@@ -6852,6 +6906,7 @@ msgstr "Pagamento scaduto"
msgid "PDF Document"
msgstr "Documento PDF"
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-status.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx
@@ -6967,6 +7022,7 @@ msgid "Please check the CSV file and make sure it is according to our format"
msgstr "Si prega di controllare il file CSV e assicurarsi che sia conforme al nostro formato"
#: apps/remix/app/components/embed/embed-document-waiting-for-turn.tsx
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Please check with the parent application for more information."
msgstr "Controlla con l'applicazione principale per ulteriori informazioni."
@@ -7406,6 +7462,10 @@ msgstr "Il destinatario ha messo in CC il documento"
msgid "Recipient completed their task"
msgstr "Il destinatario ha completato la propria attività"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Recipient expired email"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "Recipient failed to validate a 2FA token for the document"
msgstr "Il destinatario non è riuscito a convalidare un token 2FA per il documento"
@@ -7434,6 +7494,11 @@ msgstr "Il destinatario ha firmato il documento"
msgid "Recipient signing request email"
msgstr "Email richiesta firma destinatario"
#: packages/lib/utils/document-audit-logs.ts
msgctxt "Audit log format"
msgid "Recipient signing window expired"
msgstr ""
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Recipient updated"
msgstr "Destinatario aggiornato"
@@ -7789,6 +7854,7 @@ msgstr "Riprova"
msgid "Return"
msgstr "Ritorna"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
msgid "Return Home"
@@ -8209,6 +8275,10 @@ msgstr "Invia documenti ai destinatari immediatamente"
msgid "Send on Behalf of Team"
msgstr "Invia per conto del team"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send recipient expired email to the owner"
msgstr ""
#: apps/remix/app/components/dialogs/document-resend-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx
msgid "Send reminder"
@@ -8516,6 +8586,10 @@ msgstr "Certificato di firma fornito da"
msgid "Signing Complete!"
msgstr "Firma completata!"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "Signing Deadline Expired"
msgstr ""
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
msgid "Signing for"
msgstr "Firma per"
@@ -8540,6 +8614,26 @@ msgstr "I link di firma sono stati generati per questo documento."
msgid "Signing order is enabled."
msgstr "L'ordine di firma è abilitato."
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Signing Window Expired"
msgstr ""
#. placeholder {0}: recipient.name || recipient.email
#. placeholder {1}: envelope.title
#: packages/lib/jobs/definitions/emails/send-owner-recipient-expired-email.handler.ts
msgid "Signing window expired for \"{0}\" on \"{1}\""
msgstr ""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "Signing window expired for \"{displayName}\" on \"{documentName}\""
msgstr ""
#. placeholder {0}: data.recipientName || data.recipientEmail
#: packages/lib/utils/document-audit-logs.ts
#: packages/lib/utils/document-audit-logs.ts
msgid "Signing window expired for {0}"
msgstr ""
#: apps/remix/app/components/forms/signup.tsx
msgid "Signups are disabled."
msgstr "Le iscrizioni sono disabilitate."
@@ -9489,6 +9583,10 @@ msgstr "L'email del firmatario"
msgid "The signer's name"
msgstr "Il nome del firmatario"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "The signing deadline for this document has passed. Please contact the document owner if you need a new copy to sign."
msgstr ""
#: apps/remix/app/components/general/avatar-with-recipient.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
@@ -9496,6 +9594,14 @@ msgstr "Il nome del firmatario"
msgid "The signing link has been copied to your clipboard."
msgstr "Il link di firma è stato copiato negli appunti."
#: packages/email/templates/recipient-expired.tsx
msgid "The signing window for \"{recipientName}\" on document \"{documentName}\" has expired."
msgstr ""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "The signing window for {displayName} on document \"{documentName}\" has expired. You can resend the document to extend their deadline or cancel the document."
msgstr ""
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
msgid "The site banner is a message that is shown at the top of the site. It can be used to display important information to your users."
msgstr "Il banner del sito è un messaggio che viene mostrato in cima al sito. Può essere utilizzato per visualizzare informazioni importanti ai tuoi utenti."
@@ -9861,6 +9967,10 @@ msgstr "Questo sarà inviato a tutti i destinatari una volta che il documento è
msgid "This will be sent to the document owner once the document has been fully completed."
msgstr "Questo sarà inviato al proprietario del documento una volta che il documento è stato completamente completato."
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "This will be sent to the document owner when a recipient's signing window has expired."
msgstr ""
#: apps/remix/app/components/tables/organisation-email-domains-table.tsx
msgid "This will check and sync the status of all email domains for this organisation"
msgstr "Questo controllerà e sincronizzerà lo stato di tutti i domini email per questa organizzazione"
@@ -10792,6 +10902,7 @@ msgstr "Visualizza documento"
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
#: packages/email/template-components/template-document-invite.tsx
#: packages/email/template-components/template-document-rejected.tsx
#: packages/email/template-components/template-recipient-expired.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
@@ -11319,6 +11430,10 @@ msgstr "URL del webhook"
msgid "Webhooks"
msgstr "Webhook"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Weeks"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Welcome"
msgstr "Benvenuto"
@@ -11404,6 +11519,10 @@ msgstr "Scrivi una descrizione da mostrare sul tuo profilo pubblico"
msgid "Yearly"
msgstr "Annuale"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Years"
msgstr ""
#: apps/remix/app/components/forms/branding-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
@@ -12378,6 +12497,10 @@ msgstr "Il tuo codice di recupero è stato copiato negli appunti."
msgid "Your recovery codes are listed below. Please store them in a safe place."
msgstr "I tuoi codici di recupero sono elencati di seguito. Si prega di conservarli in un luogo sicuro."
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Your signing window for this document has expired. Please contact the sender for a new invitation."
msgstr ""
#: apps/remix/app/components/forms/support-ticket-form.tsx
msgid "Your support request has been submitted. We'll get back to you soon!"
msgstr "La tua richiesta di supporto è stata inviata. Ti contatteremo presto!"
@@ -12446,4 +12569,3 @@ msgstr "Il tuo codice di verifica:"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
msgid "your-domain.com another-domain.com"
msgstr "tuo-dominio.com altro-dominio.com"
+123 -1
View File
@@ -2764,6 +2764,10 @@ msgstr "ドキュメントを表示して続行してください。"
msgid "Continue to login"
msgstr "ログインを続ける"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Controls how long recipients have to complete signing before the document expires. After expiration, recipients can no longer sign the document."
msgstr ""
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Controls the default email settings when new documents or templates are created"
msgstr "新しいドキュメントやテンプレートを作成する際の既定のメール設定を制御します"
@@ -3123,6 +3127,10 @@ msgstr "現在、メールドメインは Platform プラン以上のみ設定
msgid "Custom {0} MB file"
msgstr "カスタム {0} MB ファイル"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Custom duration"
msgstr ""
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups._index.tsx
msgid "Custom Organisation Groups"
msgstr "カスタム組織グループ"
@@ -3165,6 +3173,10 @@ msgstr "日付設定"
msgid "David is the Employee, Lucas is the Manager"
msgstr "David は従業員で、Lucas はマネージャーです"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Days"
msgstr ""
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
#: packages/email/templates/organisation-invite.tsx
@@ -3191,6 +3203,10 @@ msgstr "既定のメール"
msgid "Default Email Settings"
msgstr "既定のメール設定"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Default Envelope Expiration"
msgstr ""
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
msgid "Default file"
msgstr "デフォルトのファイル"
@@ -3943,6 +3959,7 @@ msgstr "ドキュメント"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx
#: apps/remix/app/components/general/skeletons/document-edit-skeleton.tsx
#: apps/remix/app/components/general/user-profile-skeleton.tsx
#: apps/remix/app/components/general/user-profile-timur.tsx
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/tables/organisation-insights-table.tsx
@@ -4731,6 +4748,11 @@ msgstr "全員が署名しました。署名済みドキュメントのコピー
msgid "Exceeded timeout"
msgstr "タイムアウトを超えました"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "Expiration"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings._index.tsx
msgid "Expired"
msgstr "有効期限切れ"
@@ -4744,6 +4766,11 @@ msgstr "有効期限切れ"
msgid "Expires"
msgstr "有効期限"
#. placeholder {0}: recipient.expiresAt ? i18n.date(recipient.expiresAt, DateTime.DATETIME_MED) : 'N/A'
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Expires {0}"
msgstr ""
#. placeholder {0}: DateTime.fromMillis(Math.max(millisecondsRemaining, 0)).toFormat( 'mm:ss', )
#: apps/remix/app/components/general/document-signing/access-auth-2fa-form.tsx
msgid "Expires in {0}"
@@ -5283,6 +5310,10 @@ msgstr "ホーム(フォルダなし)"
msgid "Horizontal"
msgstr "横"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "この組織と自分のアカウントをリンクすることに同意します"
@@ -5385,6 +5416,10 @@ msgstr "監査ログをドキュメントに含める"
msgid "Include the Signing Certificate in the Document"
msgstr "署名証明書をドキュメントに含める"
#: apps/remix/app/components/general/billing-plans.tsx
msgid "Includes:"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-information.tsx
#: apps/remix/app/components/general/template/template-page-view-information.tsx
msgid "Information"
@@ -5406,6 +5441,7 @@ msgstr "認証方法を継承"
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Inherit from organisation"
@@ -5461,6 +5497,10 @@ msgstr "無効なドメイン"
msgid "Invalid email"
msgstr "無効なメールアドレスです"
#: apps/remix/app/routes/embed+/v1+/authoring+/_layout.tsx
msgid "Invalid embedding presign token provided"
msgstr ""
#: apps/remix/app/components/general/admin-license-card.tsx
msgid "Invalid License Key"
msgstr "無効なライセンスキー"
@@ -5814,6 +5854,8 @@ msgstr "候補を読み込み中..."
#: 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
#: packages/ui/components/pdf-viewer/pdf-viewer-konva-lazy.tsx
#: packages/ui/primitives/pdf-viewer/lazy.tsx
msgid "Loading..."
msgstr "読み込み中..."
@@ -6120,6 +6162,10 @@ msgstr "月間アクティブユーザー:1 つ以上の文書を作成した
msgid "Monthly Active Users: Users that had at least one of their documents completed"
msgstr "月間アクティブユーザー:1 つ以上の文書が完了したユーザー"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Months"
msgstr ""
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/envelopes-bulk-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
@@ -6241,6 +6287,10 @@ msgstr "なし"
msgid "Never expire"
msgstr "有効期限なし"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Never expires"
msgstr ""
#: apps/remix/app/components/forms/password.tsx
msgid "New Password"
msgstr "新しいパスワード"
@@ -6442,6 +6492,10 @@ msgstr "なし(グローバル設定を上書き)"
msgid "Not found"
msgstr "見つかりません"
#: apps/remix/app/routes/embed+/_v0+/_layout.tsx
msgid "Not Found"
msgstr ""
#: apps/remix/app/components/forms/signin.tsx
msgid "Not supported"
msgstr "サポートされていません"
@@ -6852,6 +6906,7 @@ msgstr "支払い期限超過"
msgid "PDF Document"
msgstr "PDF文書"
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-status.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx
@@ -6967,6 +7022,7 @@ msgid "Please check the CSV file and make sure it is according to our format"
msgstr "CSV ファイルが当社のフォーマットに従っているか確認してください"
#: apps/remix/app/components/embed/embed-document-waiting-for-turn.tsx
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Please check with the parent application for more information."
msgstr "詳細については、親アプリケーションを確認してください。"
@@ -7406,6 +7462,10 @@ msgstr "受信者が文書のCCに追加されました"
msgid "Recipient completed their task"
msgstr "受信者が自分のタスクを完了しました"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Recipient expired email"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "Recipient failed to validate a 2FA token for the document"
msgstr "受信者は文書用の2要素認証トークンを検証できませんでした"
@@ -7434,6 +7494,11 @@ msgstr "受信者が文書に署名しました"
msgid "Recipient signing request email"
msgstr "受信者署名依頼メール"
#: packages/lib/utils/document-audit-logs.ts
msgctxt "Audit log format"
msgid "Recipient signing window expired"
msgstr ""
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Recipient updated"
msgstr "受信者を更新しました"
@@ -7789,6 +7854,7 @@ msgstr "再試行"
msgid "Return"
msgstr "戻る"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
msgid "Return Home"
@@ -8209,6 +8275,10 @@ msgstr "ドキュメントをすぐに受信者へ送信する"
msgid "Send on Behalf of Team"
msgstr "チームを代表して送信"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send recipient expired email to the owner"
msgstr ""
#: apps/remix/app/components/dialogs/document-resend-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx
msgid "Send reminder"
@@ -8516,6 +8586,10 @@ msgstr "署名証明書の提供元"
msgid "Signing Complete!"
msgstr "署名が完了しました"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "Signing Deadline Expired"
msgstr ""
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
msgid "Signing for"
msgstr "署名対象"
@@ -8540,6 +8614,26 @@ msgstr "この文書の署名リンクが生成されています。"
msgid "Signing order is enabled."
msgstr "署名順序が有効になっています。"
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Signing Window Expired"
msgstr ""
#. placeholder {0}: recipient.name || recipient.email
#. placeholder {1}: envelope.title
#: packages/lib/jobs/definitions/emails/send-owner-recipient-expired-email.handler.ts
msgid "Signing window expired for \"{0}\" on \"{1}\""
msgstr ""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "Signing window expired for \"{displayName}\" on \"{documentName}\""
msgstr ""
#. placeholder {0}: data.recipientName || data.recipientEmail
#: packages/lib/utils/document-audit-logs.ts
#: packages/lib/utils/document-audit-logs.ts
msgid "Signing window expired for {0}"
msgstr ""
#: apps/remix/app/components/forms/signup.tsx
msgid "Signups are disabled."
msgstr "サインアップは無効になっています。"
@@ -9489,6 +9583,10 @@ msgstr "署名者のメールアドレス"
msgid "The signer's name"
msgstr "署名者の名前"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "The signing deadline for this document has passed. Please contact the document owner if you need a new copy to sign."
msgstr ""
#: apps/remix/app/components/general/avatar-with-recipient.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
@@ -9496,6 +9594,14 @@ msgstr "署名者の名前"
msgid "The signing link has been copied to your clipboard."
msgstr "署名用リンクをクリップボードにコピーしました。"
#: packages/email/templates/recipient-expired.tsx
msgid "The signing window for \"{recipientName}\" on document \"{documentName}\" has expired."
msgstr ""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "The signing window for {displayName} on document \"{documentName}\" has expired. You can resend the document to extend their deadline or cancel the document."
msgstr ""
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
msgid "The site banner is a message that is shown at the top of the site. It can be used to display important information to your users."
msgstr "サイトバナーはサイト上部に表示されるメッセージです。ユーザーに重要なお知らせを表示するために利用できます。"
@@ -9861,6 +9967,10 @@ msgstr "ドキュメントが完全に完了した後、これはすべての受
msgid "This will be sent to the document owner once the document has been fully completed."
msgstr "ドキュメントが完全に完了した後、これはドキュメント所有者に送信されます。"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "This will be sent to the document owner when a recipient's signing window has expired."
msgstr ""
#: apps/remix/app/components/tables/organisation-email-domains-table.tsx
msgid "This will check and sync the status of all email domains for this organisation"
msgstr "この操作により、この組織のすべてのメールドメインのステータスをチェックして同期します。"
@@ -10792,6 +10902,7 @@ msgstr "ドキュメントを表示"
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
#: packages/email/template-components/template-document-invite.tsx
#: packages/email/template-components/template-document-rejected.tsx
#: packages/email/template-components/template-recipient-expired.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
@@ -11319,6 +11430,10 @@ msgstr "Webhook URL"
msgid "Webhooks"
msgstr "Webhook"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Weeks"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Welcome"
msgstr "ようこそ"
@@ -11404,6 +11519,10 @@ msgstr "公開プロフィールに表示する説明文を入力してくださ
msgid "Yearly"
msgstr "年額"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Years"
msgstr ""
#: apps/remix/app/components/forms/branding-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
@@ -12378,6 +12497,10 @@ msgstr "リカバリーコードをクリップボードにコピーしました
msgid "Your recovery codes are listed below. Please store them in a safe place."
msgstr "リカバリーコードは以下のとおりです。安全な場所に保管してください。"
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Your signing window for this document has expired. Please contact the sender for a new invitation."
msgstr ""
#: apps/remix/app/components/forms/support-ticket-form.tsx
msgid "Your support request has been submitted. We'll get back to you soon!"
msgstr "サポートリクエストを送信しました。追ってご連絡いたします。"
@@ -12446,4 +12569,3 @@ msgstr "認証コード:"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
msgid "your-domain.com another-domain.com"
msgstr "your-domain.com another-domain.com"
+123 -1
View File
@@ -2764,6 +2764,10 @@ msgstr "문서를 열람하여 계속 진행하세요."
msgid "Continue to login"
msgstr "로그인으로 이동"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Controls how long recipients have to complete signing before the document expires. After expiration, recipients can no longer sign the document."
msgstr ""
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Controls the default email settings when new documents or templates are created"
msgstr "새 문서 또는 템플릿을 생성할 때 사용할 기본 이메일 설정을 제어합니다."
@@ -3123,6 +3127,10 @@ msgstr "현재 이메일 도메인은 Platform 요금제 이상에서만 구성
msgid "Custom {0} MB file"
msgstr "사용자 정의 {0} MB 파일"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Custom duration"
msgstr ""
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups._index.tsx
msgid "Custom Organisation Groups"
msgstr "사용자 지정 조직 그룹"
@@ -3165,6 +3173,10 @@ msgstr "날짜 설정"
msgid "David is the Employee, Lucas is the Manager"
msgstr "David는 직원이고, Lucas는 관리자입니다."
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Days"
msgstr ""
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
#: packages/email/templates/organisation-invite.tsx
@@ -3191,6 +3203,10 @@ msgstr "기본 이메일"
msgid "Default Email Settings"
msgstr "기본 이메일 설정"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Default Envelope Expiration"
msgstr ""
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
msgid "Default file"
msgstr "기본 파일"
@@ -3943,6 +3959,7 @@ msgstr "문서"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx
#: apps/remix/app/components/general/skeletons/document-edit-skeleton.tsx
#: apps/remix/app/components/general/user-profile-skeleton.tsx
#: apps/remix/app/components/general/user-profile-timur.tsx
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/tables/organisation-insights-table.tsx
@@ -4731,6 +4748,11 @@ msgstr "모두 서명했습니다! 서명된 문서의 사본이 이메일로
msgid "Exceeded timeout"
msgstr "시간 초과됨"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "Expiration"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings._index.tsx
msgid "Expired"
msgstr "만료됨"
@@ -4744,6 +4766,11 @@ msgstr "만료됨"
msgid "Expires"
msgstr "만료일"
#. placeholder {0}: recipient.expiresAt ? i18n.date(recipient.expiresAt, DateTime.DATETIME_MED) : 'N/A'
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Expires {0}"
msgstr ""
#. placeholder {0}: DateTime.fromMillis(Math.max(millisecondsRemaining, 0)).toFormat( 'mm:ss', )
#: apps/remix/app/components/general/document-signing/access-auth-2fa-form.tsx
msgid "Expires in {0}"
@@ -5283,6 +5310,10 @@ msgstr "홈(폴더 없음)"
msgid "Horizontal"
msgstr "가로"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "이 조직과 계정을 연결하는 데 동의합니다"
@@ -5385,6 +5416,10 @@ msgstr "문서에 감사 로그 포함"
msgid "Include the Signing Certificate in the Document"
msgstr "문서에 서명 인증서 포함"
#: apps/remix/app/components/general/billing-plans.tsx
msgid "Includes:"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-information.tsx
#: apps/remix/app/components/general/template/template-page-view-information.tsx
msgid "Information"
@@ -5406,6 +5441,7 @@ msgstr "인증 방식 상속"
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Inherit from organisation"
@@ -5461,6 +5497,10 @@ msgstr "잘못된 도메인"
msgid "Invalid email"
msgstr "유효한 이메일이 아닙니다."
#: apps/remix/app/routes/embed+/v1+/authoring+/_layout.tsx
msgid "Invalid embedding presign token provided"
msgstr ""
#: apps/remix/app/components/general/admin-license-card.tsx
msgid "Invalid License Key"
msgstr "잘못된 라이선스 키"
@@ -5814,6 +5854,8 @@ msgstr "제안 불러오는 중..."
#: 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
#: packages/ui/components/pdf-viewer/pdf-viewer-konva-lazy.tsx
#: packages/ui/primitives/pdf-viewer/lazy.tsx
msgid "Loading..."
msgstr "로딩 중..."
@@ -6120,6 +6162,10 @@ msgstr "월간 활성 사용자: 문서를 하나 이상 생성한 사용자"
msgid "Monthly Active Users: Users that had at least one of their documents completed"
msgstr "월간 활성 사용자: 문서가 하나 이상 완료된 사용자"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Months"
msgstr ""
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/envelopes-bulk-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
@@ -6241,6 +6287,10 @@ msgstr "없음"
msgid "Never expire"
msgstr "만료되지 않음"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Never expires"
msgstr ""
#: apps/remix/app/components/forms/password.tsx
msgid "New Password"
msgstr "새 비밀번호"
@@ -6442,6 +6492,10 @@ msgstr "없음(전역 설정 무시)"
msgid "Not found"
msgstr "찾을 수 없음"
#: apps/remix/app/routes/embed+/_v0+/_layout.tsx
msgid "Not Found"
msgstr ""
#: apps/remix/app/components/forms/signin.tsx
msgid "Not supported"
msgstr "지원되지 않습니다"
@@ -6852,6 +6906,7 @@ msgstr "결제 지연"
msgid "PDF Document"
msgstr "PDF 문서"
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-status.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx
@@ -6967,6 +7022,7 @@ msgid "Please check the CSV file and make sure it is according to our format"
msgstr "CSV 파일 형식을 확인하고, 안내된 형식과 일치하는지 확인해 주세요"
#: apps/remix/app/components/embed/embed-document-waiting-for-turn.tsx
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Please check with the parent application for more information."
msgstr "자세한 정보는 상위 애플리케이션에서 확인해 주세요."
@@ -7406,6 +7462,10 @@ msgstr "수신자가 문서의 참조(CC) 수신자가 되었습니다"
msgid "Recipient completed their task"
msgstr "수신자가 자신의 작업을 완료했습니다"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Recipient expired email"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "Recipient failed to validate a 2FA token for the document"
msgstr "수신자가 문서에 대한 2FA 토큰 검증에 실패했습니다"
@@ -7434,6 +7494,11 @@ msgstr "수신자가 문서에 서명했습니다"
msgid "Recipient signing request email"
msgstr "수신자 서명 요청 이메일"
#: packages/lib/utils/document-audit-logs.ts
msgctxt "Audit log format"
msgid "Recipient signing window expired"
msgstr ""
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Recipient updated"
msgstr "수신자가 업데이트되었습니다"
@@ -7789,6 +7854,7 @@ msgstr "재시도"
msgid "Return"
msgstr "돌아가기"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
msgid "Return Home"
@@ -8209,6 +8275,10 @@ msgstr "생성된 문서를 즉시 수신자에게 전송"
msgid "Send on Behalf of Team"
msgstr "팀을 대신해 발송"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send recipient expired email to the owner"
msgstr ""
#: apps/remix/app/components/dialogs/document-resend-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx
msgid "Send reminder"
@@ -8516,6 +8586,10 @@ msgstr "서명 인증서 제공자"
msgid "Signing Complete!"
msgstr "서명이 완료되었습니다!"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "Signing Deadline Expired"
msgstr ""
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
msgid "Signing for"
msgstr "대리 서명 대상"
@@ -8540,6 +8614,26 @@ msgstr "이 문서에 대한 서명 링크가 생성되었습니다."
msgid "Signing order is enabled."
msgstr "서명 순서가 활성화되어 있습니다."
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Signing Window Expired"
msgstr ""
#. placeholder {0}: recipient.name || recipient.email
#. placeholder {1}: envelope.title
#: packages/lib/jobs/definitions/emails/send-owner-recipient-expired-email.handler.ts
msgid "Signing window expired for \"{0}\" on \"{1}\""
msgstr ""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "Signing window expired for \"{displayName}\" on \"{documentName}\""
msgstr ""
#. placeholder {0}: data.recipientName || data.recipientEmail
#: packages/lib/utils/document-audit-logs.ts
#: packages/lib/utils/document-audit-logs.ts
msgid "Signing window expired for {0}"
msgstr ""
#: apps/remix/app/components/forms/signup.tsx
msgid "Signups are disabled."
msgstr "회원 가입이 비활성화되어 있습니다."
@@ -9489,6 +9583,10 @@ msgstr "서명자의 이메일"
msgid "The signer's name"
msgstr "서명자의 이름"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "The signing deadline for this document has passed. Please contact the document owner if you need a new copy to sign."
msgstr ""
#: apps/remix/app/components/general/avatar-with-recipient.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
@@ -9496,6 +9594,14 @@ msgstr "서명자의 이름"
msgid "The signing link has been copied to your clipboard."
msgstr "서명 링크가 클립보드에 복사되었습니다."
#: packages/email/templates/recipient-expired.tsx
msgid "The signing window for \"{recipientName}\" on document \"{documentName}\" has expired."
msgstr ""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "The signing window for {displayName} on document \"{documentName}\" has expired. You can resend the document to extend their deadline or cancel the document."
msgstr ""
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
msgid "The site banner is a message that is shown at the top of the site. It can be used to display important information to your users."
msgstr "사이트 배너는 사이트 상단에 표시되는 메시지입니다. 사용자에게 중요한 정보를 표시하는 데 사용할 수 있습니다."
@@ -9861,6 +9967,10 @@ msgstr "문서가 완전히 완료되면 모든 수신자에게 이 메일이
msgid "This will be sent to the document owner once the document has been fully completed."
msgstr "문서가 완전히 완료되면 문서 소유자에게 이 메일이 전송됩니다."
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "This will be sent to the document owner when a recipient's signing window has expired."
msgstr ""
#: apps/remix/app/components/tables/organisation-email-domains-table.tsx
msgid "This will check and sync the status of all email domains for this organisation"
msgstr "이 작업은 이 조직의 모든 이메일 도메인 상태를 확인하고 동기화합니다."
@@ -10792,6 +10902,7 @@ msgstr "문서 보기"
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
#: packages/email/template-components/template-document-invite.tsx
#: packages/email/template-components/template-document-rejected.tsx
#: packages/email/template-components/template-recipient-expired.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
@@ -11319,6 +11430,10 @@ msgstr "웹훅 URL"
msgid "Webhooks"
msgstr "웹훅"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Weeks"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Welcome"
msgstr "환영합니다"
@@ -11404,6 +11519,10 @@ msgstr "공개 프로필에 표시될 설명을 작성하세요."
msgid "Yearly"
msgstr "연간"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Years"
msgstr ""
#: apps/remix/app/components/forms/branding-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
@@ -12378,6 +12497,10 @@ msgstr "복구 코드가 클립보드에 복사되었습니다."
msgid "Your recovery codes are listed below. Please store them in a safe place."
msgstr "아래에 복구 코드가 표시됩니다. 안전한 곳에 보관해 주세요."
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Your signing window for this document has expired. Please contact the sender for a new invitation."
msgstr ""
#: apps/remix/app/components/forms/support-ticket-form.tsx
msgid "Your support request has been submitted. We'll get back to you soon!"
msgstr "지원 요청이 제출되었습니다. 곧 다시 연락드리겠습니다!"
@@ -12446,4 +12569,3 @@ msgstr "인증 코드:"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
msgid "your-domain.com another-domain.com"
msgstr "your-domain.com another-domain.com"
+123 -1
View File
@@ -2764,6 +2764,10 @@ msgstr "Ga verder door het document te bekijken."
msgid "Continue to login"
msgstr "Doorgaan naar inloggen"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Controls how long recipients have to complete signing before the document expires. After expiration, recipients can no longer sign the document."
msgstr ""
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Controls the default email settings when new documents or templates are created"
msgstr "Bepaalt de standaard e-mailinstellingen wanneer nieuwe documenten of sjablonen worden aangemaakt"
@@ -3123,6 +3127,10 @@ msgstr "E-maildomeinen kunnen momenteel alleen worden geconfigureerd voor Platfo
msgid "Custom {0} MB file"
msgstr "Aangepast {0} MB-bestand"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Custom duration"
msgstr ""
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups._index.tsx
msgid "Custom Organisation Groups"
msgstr "Aangepaste organisatiegroepen"
@@ -3165,6 +3173,10 @@ msgstr "Datuminstellingen"
msgid "David is the Employee, Lucas is the Manager"
msgstr "David is de werknemer, Lucas is de manager"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Days"
msgstr ""
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
#: packages/email/templates/organisation-invite.tsx
@@ -3191,6 +3203,10 @@ msgstr "Standaard e-mailadres"
msgid "Default Email Settings"
msgstr "Standaarde-mailinstellingen"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Default Envelope Expiration"
msgstr ""
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
msgid "Default file"
msgstr "Standaardbestand"
@@ -3943,6 +3959,7 @@ msgstr "Documentatie"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx
#: apps/remix/app/components/general/skeletons/document-edit-skeleton.tsx
#: apps/remix/app/components/general/user-profile-skeleton.tsx
#: apps/remix/app/components/general/user-profile-timur.tsx
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/tables/organisation-insights-table.tsx
@@ -4731,6 +4748,11 @@ msgstr "Iedereen heeft ondertekend! U ontvangt een kopie van het ondertekende do
msgid "Exceeded timeout"
msgstr "Timeout overschreden"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "Expiration"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings._index.tsx
msgid "Expired"
msgstr "Verlopen"
@@ -4744,6 +4766,11 @@ msgstr "Verlopen"
msgid "Expires"
msgstr "Verloopt"
#. placeholder {0}: recipient.expiresAt ? i18n.date(recipient.expiresAt, DateTime.DATETIME_MED) : 'N/A'
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Expires {0}"
msgstr ""
#. placeholder {0}: DateTime.fromMillis(Math.max(millisecondsRemaining, 0)).toFormat( 'mm:ss', )
#: apps/remix/app/components/general/document-signing/access-auth-2fa-form.tsx
msgid "Expires in {0}"
@@ -5283,6 +5310,10 @@ msgstr "Home (geen map)"
msgid "Horizontal"
msgstr "Horizontaal"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "Ik ga akkoord met het koppelen van mijn account aan deze organisatie"
@@ -5385,6 +5416,10 @@ msgstr "Auditlogs opnemen in het document"
msgid "Include the Signing Certificate in the Document"
msgstr "Het ondertekeningscertificaat opnemen in het document"
#: apps/remix/app/components/general/billing-plans.tsx
msgid "Includes:"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-information.tsx
#: apps/remix/app/components/general/template/template-page-view-information.tsx
msgid "Information"
@@ -5406,6 +5441,7 @@ msgstr "Authenticatiemethode overnemen"
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Inherit from organisation"
@@ -5461,6 +5497,10 @@ msgstr "Ongeldige domeinen"
msgid "Invalid email"
msgstr "Ongeldig e-mailadres"
#: apps/remix/app/routes/embed+/v1+/authoring+/_layout.tsx
msgid "Invalid embedding presign token provided"
msgstr ""
#: apps/remix/app/components/general/admin-license-card.tsx
msgid "Invalid License Key"
msgstr "Ongeldige licentiesleutel"
@@ -5814,6 +5854,8 @@ msgstr "Suggesties laden..."
#: 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
#: packages/ui/components/pdf-viewer/pdf-viewer-konva-lazy.tsx
#: packages/ui/primitives/pdf-viewer/lazy.tsx
msgid "Loading..."
msgstr "Laden..."
@@ -6120,6 +6162,10 @@ msgstr "Maandelijks actieve gebruikers: gebruikers die ten minste één document
msgid "Monthly Active Users: Users that had at least one of their documents completed"
msgstr "Maandelijks actieve gebruikers: gebruikers van wie ten minste één document is voltooid"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Months"
msgstr ""
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/envelopes-bulk-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
@@ -6241,6 +6287,10 @@ msgstr "Nooit"
msgid "Never expire"
msgstr "Nooit verlopen"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Never expires"
msgstr ""
#: apps/remix/app/components/forms/password.tsx
msgid "New Password"
msgstr "Nieuw wachtwoord"
@@ -6442,6 +6492,10 @@ msgstr "Geen (Overschrijft globale instellingen)"
msgid "Not found"
msgstr "Niet gevonden"
#: apps/remix/app/routes/embed+/_v0+/_layout.tsx
msgid "Not Found"
msgstr ""
#: apps/remix/app/components/forms/signin.tsx
msgid "Not supported"
msgstr "Niet ondersteund"
@@ -6852,6 +6906,7 @@ msgstr "Betaling achterstallig"
msgid "PDF Document"
msgstr "PDF-document"
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-status.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx
@@ -6967,6 +7022,7 @@ msgid "Please check the CSV file and make sure it is according to our format"
msgstr "Controleer het CSVbestand en zorg dat het overeenkomt met ons formaat"
#: apps/remix/app/components/embed/embed-document-waiting-for-turn.tsx
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Please check with the parent application for more information."
msgstr "Raadpleeg de hoofdapplicatie voor meer informatie."
@@ -7406,6 +7462,10 @@ msgstr "Ontvanger heeft het document in CC gekregen"
msgid "Recipient completed their task"
msgstr "Ontvanger heeft zijn taak voltooid"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Recipient expired email"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "Recipient failed to validate a 2FA token for the document"
msgstr "Ontvanger kon een 2FA-token voor het document niet valideren"
@@ -7434,6 +7494,11 @@ msgstr "Ontvanger heeft het document ondertekend"
msgid "Recipient signing request email"
msgstr "E-mail voor ondertekeningsverzoek aan ontvanger"
#: packages/lib/utils/document-audit-logs.ts
msgctxt "Audit log format"
msgid "Recipient signing window expired"
msgstr ""
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Recipient updated"
msgstr "Ontvanger bijgewerkt"
@@ -7789,6 +7854,7 @@ msgstr "Opnieuw proberen"
msgid "Return"
msgstr "Terugkeren"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
msgid "Return Home"
@@ -8209,6 +8275,10 @@ msgstr "Documenten direct naar ontvangers verzenden"
msgid "Send on Behalf of Team"
msgstr "Verzenden namens team"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send recipient expired email to the owner"
msgstr ""
#: apps/remix/app/components/dialogs/document-resend-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx
msgid "Send reminder"
@@ -8516,6 +8586,10 @@ msgstr "Ondertekeningscertificaat verstrekt door"
msgid "Signing Complete!"
msgstr "Ondertekening voltooid!"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "Signing Deadline Expired"
msgstr ""
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
msgid "Signing for"
msgstr "Ondertekenen voor"
@@ -8540,6 +8614,26 @@ msgstr "Voor dit document zijn ondertekeningslinks gegenereerd."
msgid "Signing order is enabled."
msgstr "Ondertekeningsvolgorde is ingeschakeld."
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Signing Window Expired"
msgstr ""
#. placeholder {0}: recipient.name || recipient.email
#. placeholder {1}: envelope.title
#: packages/lib/jobs/definitions/emails/send-owner-recipient-expired-email.handler.ts
msgid "Signing window expired for \"{0}\" on \"{1}\""
msgstr ""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "Signing window expired for \"{displayName}\" on \"{documentName}\""
msgstr ""
#. placeholder {0}: data.recipientName || data.recipientEmail
#: packages/lib/utils/document-audit-logs.ts
#: packages/lib/utils/document-audit-logs.ts
msgid "Signing window expired for {0}"
msgstr ""
#: apps/remix/app/components/forms/signup.tsx
msgid "Signups are disabled."
msgstr "Aanmeldingen zijn uitgeschakeld."
@@ -9489,6 +9583,10 @@ msgstr "Het e-mailadres van de ondertekenaar"
msgid "The signer's name"
msgstr "De naam van de ondertekenaar"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "The signing deadline for this document has passed. Please contact the document owner if you need a new copy to sign."
msgstr ""
#: apps/remix/app/components/general/avatar-with-recipient.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
@@ -9496,6 +9594,14 @@ msgstr "De naam van de ondertekenaar"
msgid "The signing link has been copied to your clipboard."
msgstr "De ondertekeningslink is naar je klembord gekopieerd."
#: packages/email/templates/recipient-expired.tsx
msgid "The signing window for \"{recipientName}\" on document \"{documentName}\" has expired."
msgstr ""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "The signing window for {displayName} on document \"{documentName}\" has expired. You can resend the document to extend their deadline or cancel the document."
msgstr ""
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
msgid "The site banner is a message that is shown at the top of the site. It can be used to display important information to your users."
msgstr "De sitebanner is een bericht dat bovenaan de site wordt weergegeven. Je kunt hiermee belangrijke informatie aan je gebruikers tonen."
@@ -9861,6 +9967,10 @@ msgstr "Dit wordt verzonden naar alle ontvangers zodra het document volledig is
msgid "This will be sent to the document owner once the document has been fully completed."
msgstr "Dit wordt verzonden naar de documenteigenaar zodra het document volledig is voltooid."
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "This will be sent to the document owner when a recipient's signing window has expired."
msgstr ""
#: apps/remix/app/components/tables/organisation-email-domains-table.tsx
msgid "This will check and sync the status of all email domains for this organisation"
msgstr "Hiermee wordt de status van alle e-maildomeinen voor deze organisatie gecontroleerd en gesynchroniseerd"
@@ -10792,6 +10902,7 @@ msgstr "Document bekijken"
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
#: packages/email/template-components/template-document-invite.tsx
#: packages/email/template-components/template-document-rejected.tsx
#: packages/email/template-components/template-recipient-expired.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
@@ -11319,6 +11430,10 @@ msgstr "WebhookURL"
msgid "Webhooks"
msgstr "Webhooks"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Weeks"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Welcome"
msgstr "Welkom"
@@ -11404,6 +11519,10 @@ msgstr "Schrijf een beschrijving die op je openbare profiel wordt weergegeven"
msgid "Yearly"
msgstr "Jaarlijks"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Years"
msgstr ""
#: apps/remix/app/components/forms/branding-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
@@ -12378,6 +12497,10 @@ msgstr "Je herstelcode is naar je klembord gekopieerd."
msgid "Your recovery codes are listed below. Please store them in a safe place."
msgstr "Je herstelcodes staan hieronder. Bewaar ze op een veilige plek."
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Your signing window for this document has expired. Please contact the sender for a new invitation."
msgstr ""
#: apps/remix/app/components/forms/support-ticket-form.tsx
msgid "Your support request has been submitted. We'll get back to you soon!"
msgstr "Uw supportverzoek is ingediend. We nemen snel contact met u op!"
@@ -12446,4 +12569,3 @@ msgstr "Uw verificatiecode:"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
msgid "your-domain.com another-domain.com"
msgstr "your-domain.com another-domain.com"
+123 -1
View File
@@ -2764,6 +2764,10 @@ msgstr "Wyświetl dokument."
msgid "Continue to login"
msgstr "Przejdź do logowania"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Controls how long recipients have to complete signing before the document expires. After expiration, recipients can no longer sign the document."
msgstr ""
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Controls the default email settings when new documents or templates are created"
msgstr "Wybierz domyślne ustawienia wiadomości podczas tworzenia nowych dokumentów i szablonów"
@@ -3123,6 +3127,10 @@ msgstr "Domeny możesz skonfigurować tylko w planie Platform lub wyższym."
msgid "Custom {0} MB file"
msgstr "Niestandardowy plik {0} MB"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Custom duration"
msgstr ""
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups._index.tsx
msgid "Custom Organisation Groups"
msgstr "Niestandardowe grupy w organizacji"
@@ -3165,6 +3173,10 @@ msgstr "Ustawienia daty"
msgid "David is the Employee, Lucas is the Manager"
msgstr "David jest pracownikiem. Lucas jest managerem."
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Days"
msgstr ""
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
#: packages/email/templates/organisation-invite.tsx
@@ -3191,6 +3203,10 @@ msgstr "Domyślny adres e-mail"
msgid "Default Email Settings"
msgstr "Domyślne ustawienia adresu e-mail"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Default Envelope Expiration"
msgstr ""
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
msgid "Default file"
msgstr "Domyślny plik"
@@ -3943,6 +3959,7 @@ msgstr "Dokumentacja"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx
#: apps/remix/app/components/general/skeletons/document-edit-skeleton.tsx
#: apps/remix/app/components/general/user-profile-skeleton.tsx
#: apps/remix/app/components/general/user-profile-timur.tsx
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/tables/organisation-insights-table.tsx
@@ -4731,6 +4748,11 @@ msgstr "Wszyscy podpisali! Otrzymasz wiadomość z podpisanym dokumentem."
msgid "Exceeded timeout"
msgstr "Przekroczono limit czasu"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "Expiration"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings._index.tsx
msgid "Expired"
msgstr "Wygasł"
@@ -4744,6 +4766,11 @@ msgstr "Wygasła"
msgid "Expires"
msgstr "Wygasa"
#. placeholder {0}: recipient.expiresAt ? i18n.date(recipient.expiresAt, DateTime.DATETIME_MED) : 'N/A'
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Expires {0}"
msgstr ""
#. placeholder {0}: DateTime.fromMillis(Math.max(millisecondsRemaining, 0)).toFormat( 'mm:ss', )
#: apps/remix/app/components/general/document-signing/access-auth-2fa-form.tsx
msgid "Expires in {0}"
@@ -5283,6 +5310,10 @@ msgstr "Strona główna (brak folderu)"
msgid "Horizontal"
msgstr "Poziomo"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "Zgadzam się połączyć moje konto z organizacją"
@@ -5385,6 +5416,10 @@ msgstr "Dołącz dziennik logów do dokumentu"
msgid "Include the Signing Certificate in the Document"
msgstr "Dołącz certyfikat podpisu do dokumentu"
#: apps/remix/app/components/general/billing-plans.tsx
msgid "Includes:"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-information.tsx
#: apps/remix/app/components/general/template/template-page-view-information.tsx
msgid "Information"
@@ -5406,6 +5441,7 @@ msgstr "Odziedzicz metodę uwierzytelniania"
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Inherit from organisation"
@@ -5461,6 +5497,10 @@ msgstr "Domeny są nieprawidłowe"
msgid "Invalid email"
msgstr "Adres e-mail jest nieprawidłowy"
#: apps/remix/app/routes/embed+/v1+/authoring+/_layout.tsx
msgid "Invalid embedding presign token provided"
msgstr ""
#: apps/remix/app/components/general/admin-license-card.tsx
msgid "Invalid License Key"
msgstr "Klucz licencyjny jest nieprawidłowy"
@@ -5814,6 +5854,8 @@ msgstr "Ładowanie sugestii..."
#: 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
#: packages/ui/components/pdf-viewer/pdf-viewer-konva-lazy.tsx
#: packages/ui/primitives/pdf-viewer/lazy.tsx
msgid "Loading..."
msgstr "Ładowanie..."
@@ -6120,6 +6162,10 @@ msgstr "Miesięczna liczba aktywnych użytkowników: Użytkownicy, którzy utwor
msgid "Monthly Active Users: Users that had at least one of their documents completed"
msgstr "Miesięczna liczba aktywnych użytkowników: Użytkownicy, którzy zakończyli co najmniej jeden dokument"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Months"
msgstr ""
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/envelopes-bulk-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
@@ -6241,6 +6287,10 @@ msgstr "Nigdy"
msgid "Never expire"
msgstr "Nigdy nie wygasa"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Never expires"
msgstr ""
#: apps/remix/app/components/forms/password.tsx
msgid "New Password"
msgstr "Nowe hasło"
@@ -6442,6 +6492,10 @@ msgstr "Brak (odziedzicz metodę uwierzytelniania)"
msgid "Not found"
msgstr "Strona nie została znaleziona"
#: apps/remix/app/routes/embed+/_v0+/_layout.tsx
msgid "Not Found"
msgstr ""
#: apps/remix/app/components/forms/signin.tsx
msgid "Not supported"
msgstr "Nieobsługiwane"
@@ -6852,6 +6906,7 @@ msgstr "Zaległa płatność"
msgid "PDF Document"
msgstr "Dokument PDF"
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-status.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx
@@ -6967,6 +7022,7 @@ msgid "Please check the CSV file and make sure it is according to our format"
msgstr "Sprawdź plik CSV i upewnij się, że jest zgodny z wymaganym formatem"
#: apps/remix/app/components/embed/embed-document-waiting-for-turn.tsx
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Please check with the parent application for more information."
msgstr "Sprawdź instrukcje na stronie."
@@ -7406,6 +7462,10 @@ msgstr "Odbiorca odebrał kopię dokumentu"
msgid "Recipient completed their task"
msgstr "Odbiorca zakończył swoje zadanie"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Recipient expired email"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "Recipient failed to validate a 2FA token for the document"
msgstr "Odbiorca nie zweryfikował kodu weryfikacyjnego dla dokumentu"
@@ -7434,6 +7494,11 @@ msgstr "Odbiorca podpisał dokument"
msgid "Recipient signing request email"
msgstr "Wiadomość z prośbą o podpisanie"
#: packages/lib/utils/document-audit-logs.ts
msgctxt "Audit log format"
msgid "Recipient signing window expired"
msgstr ""
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Recipient updated"
msgstr "Odbiorca został zaktualizowany"
@@ -7789,6 +7854,7 @@ msgstr "Spróbuj ponownie"
msgid "Return"
msgstr "Wróć"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
msgid "Return Home"
@@ -8209,6 +8275,10 @@ msgstr "Wyślij dokumenty do odbiorców natychmiast"
msgid "Send on Behalf of Team"
msgstr "Wyślij w imieniu zespołu"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send recipient expired email to the owner"
msgstr ""
#: apps/remix/app/components/dialogs/document-resend-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx
msgid "Send reminder"
@@ -8516,6 +8586,10 @@ msgstr "Certyfikat podpisu został dostarczony przez"
msgid "Signing Complete!"
msgstr "Podpisywanie zostało zakończone!"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "Signing Deadline Expired"
msgstr ""
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
msgid "Signing for"
msgstr "Podpisywanie w imieniu"
@@ -8540,6 +8614,26 @@ msgstr "Linki do podpisywania zostały wygenerowane dla tego dokumentu."
msgid "Signing order is enabled."
msgstr "Kolejność podpisywania jest włączona."
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Signing Window Expired"
msgstr ""
#. placeholder {0}: recipient.name || recipient.email
#. placeholder {1}: envelope.title
#: packages/lib/jobs/definitions/emails/send-owner-recipient-expired-email.handler.ts
msgid "Signing window expired for \"{0}\" on \"{1}\""
msgstr ""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "Signing window expired for \"{displayName}\" on \"{documentName}\""
msgstr ""
#. placeholder {0}: data.recipientName || data.recipientEmail
#: packages/lib/utils/document-audit-logs.ts
#: packages/lib/utils/document-audit-logs.ts
msgid "Signing window expired for {0}"
msgstr ""
#: apps/remix/app/components/forms/signup.tsx
msgid "Signups are disabled."
msgstr "Rejestracje są wyłączone."
@@ -9489,6 +9583,10 @@ msgstr "Adres e-mail podpisującego"
msgid "The signer's name"
msgstr "Nazwa podpisującego"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "The signing deadline for this document has passed. Please contact the document owner if you need a new copy to sign."
msgstr ""
#: apps/remix/app/components/general/avatar-with-recipient.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
@@ -9496,6 +9594,14 @@ msgstr "Nazwa podpisującego"
msgid "The signing link has been copied to your clipboard."
msgstr "Link do podpisywania został skopiowany do schowka."
#: packages/email/templates/recipient-expired.tsx
msgid "The signing window for \"{recipientName}\" on document \"{documentName}\" has expired."
msgstr ""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "The signing window for {displayName} on document \"{documentName}\" has expired. You can resend the document to extend their deadline or cancel the document."
msgstr ""
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
msgid "The site banner is a message that is shown at the top of the site. It can be used to display important information to your users."
msgstr "Baner strony internetowej to zawartość wyświetlana na górze strony. Może być używana do wyświetlania ważnych informacji użytkownikom."
@@ -9861,6 +9967,10 @@ msgstr "Zostanie wysłana do wszystkich odbiorców po zakończeniu dokumentu."
msgid "This will be sent to the document owner once the document has been fully completed."
msgstr "Zostanie wysłana do właściciela po zakończeniu dokumentu."
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "This will be sent to the document owner when a recipient's signing window has expired."
msgstr ""
#: apps/remix/app/components/tables/organisation-email-domains-table.tsx
msgid "This will check and sync the status of all email domains for this organisation"
msgstr "Spowoduje to synchronizowanie statusu wszystkich domen organizacji"
@@ -10792,6 +10902,7 @@ msgstr "Wyświetl dokument"
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
#: packages/email/template-components/template-document-invite.tsx
#: packages/email/template-components/template-document-rejected.tsx
#: packages/email/template-components/template-recipient-expired.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
@@ -11319,6 +11430,10 @@ msgstr "Adres URL webhooka"
msgid "Webhooks"
msgstr "Webhooki"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Weeks"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Welcome"
msgstr "Witaj"
@@ -11404,6 +11519,10 @@ msgstr "Wpisz opis, który będzie wyświetlany w profilu publicznym"
msgid "Yearly"
msgstr "Rocznie"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Years"
msgstr ""
#: apps/remix/app/components/forms/branding-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
@@ -12378,6 +12497,10 @@ msgstr "Kod odzyskiwania został skopiowany do schowka."
msgid "Your recovery codes are listed below. Please store them in a safe place."
msgstr "To są Twoje kody odzyskiwania. Przechowuj je w bezpiecznym miejscu."
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Your signing window for this document has expired. Please contact the sender for a new invitation."
msgstr ""
#: apps/remix/app/components/forms/support-ticket-form.tsx
msgid "Your support request has been submitted. We'll get back to you soon!"
msgstr "Zgłoszenie zostało wysłane. Skontaktujemy się z Tobą wkrótce!"
@@ -12446,4 +12569,3 @@ msgstr "Twój kod weryfikacyjny:"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
msgid "your-domain.com another-domain.com"
msgstr "your-domain.com another-domain.com"
+149 -26
View File
@@ -2759,6 +2759,10 @@ msgstr "Continue visualizando o documento."
msgid "Continue to login"
msgstr "Continuar para o login"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Controls how long recipients have to complete signing before the document expires. After expiration, recipients can no longer sign the document."
msgstr ""
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Controls the default email settings when new documents or templates are created"
msgstr "Controla as configurações padrão de e-mail quando novos documentos ou modelos são criados"
@@ -3118,6 +3122,10 @@ msgstr "Atualmente, domínios de e-mail só podem ser configurados para planos P
msgid "Custom {0} MB file"
msgstr "Arquivo personalizado de {0} MB"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Custom duration"
msgstr ""
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups._index.tsx
msgid "Custom Organisation Groups"
msgstr "Grupos de Organização Personalizados"
@@ -3160,6 +3168,10 @@ msgstr "Configurações de Data"
msgid "David is the Employee, Lucas is the Manager"
msgstr "David é o Funcionário, Lucas é o Gerente"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Days"
msgstr ""
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
#: packages/email/templates/organisation-invite.tsx
@@ -3186,6 +3198,10 @@ msgstr "E-mail Padrão"
msgid "Default Email Settings"
msgstr "Configurações de E-mail Padrão"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Default Envelope Expiration"
msgstr ""
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
msgid "Default file"
msgstr "Arquivo padrão"
@@ -3938,6 +3954,7 @@ msgstr "Documentação"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx
#: apps/remix/app/components/general/skeletons/document-edit-skeleton.tsx
#: apps/remix/app/components/general/user-profile-skeleton.tsx
#: apps/remix/app/components/general/user-profile-timur.tsx
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/tables/organisation-insights-table.tsx
@@ -4298,6 +4315,22 @@ msgstr "Preferências de E-mail"
msgid "Email preferences updated"
msgstr "Preferências de e-mail atualizadas"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Email recipients when a pending document is deleted"
msgstr ""
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Email recipients when the document is completed"
msgstr ""
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Email recipients when they're removed from a pending document"
msgstr ""
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Email recipients with a signing request"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgctxt "Audit log format"
msgid "Email resent"
@@ -4323,6 +4356,18 @@ msgstr "E-mail enviado!"
msgid "Email Settings"
msgstr "Configurações de E-mail"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Email the owner when a recipient signs"
msgstr ""
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Email the owner when the document is completed"
msgstr ""
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Email the signer if the document is still pending"
msgstr ""
#: apps/remix/app/components/general/document-signing/access-auth-2fa-form.tsx
msgid "Email verification"
msgstr "Verificação de e-mail"
@@ -4698,6 +4743,11 @@ msgstr "Todos assinaram! Você receberá uma cópia do documento assinado por e-
msgid "Exceeded timeout"
msgstr "Tempo limite excedido"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "Expiration"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings._index.tsx
msgid "Expired"
msgstr "Expirado"
@@ -4711,6 +4761,11 @@ msgstr ""
msgid "Expires"
msgstr ""
#. placeholder {0}: recipient.expiresAt ? i18n.date(recipient.expiresAt, DateTime.DATETIME_MED) : 'N/A'
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Expires {0}"
msgstr ""
#. placeholder {0}: DateTime.fromMillis(Math.max(millisecondsRemaining, 0)).toFormat( 'mm:ss', )
#: apps/remix/app/components/general/document-signing/access-auth-2fa-form.tsx
msgid "Expires in {0}"
@@ -5250,6 +5305,10 @@ msgstr "Início (Sem Pasta)"
msgid "Horizontal"
msgstr "Horizontal"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "Concordo em vincular minha conta a esta organização"
@@ -5352,6 +5411,10 @@ msgstr "Incluir os Logs de Auditoria no Documento"
msgid "Include the Signing Certificate in the Document"
msgstr "Incluir o Certificado de Assinatura no Documento"
#: apps/remix/app/components/general/billing-plans.tsx
msgid "Includes:"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-information.tsx
#: apps/remix/app/components/general/template/template-page-view-information.tsx
msgid "Information"
@@ -5373,6 +5436,7 @@ msgstr "Herdar método de autenticação"
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Inherit from organisation"
@@ -5428,6 +5492,10 @@ msgstr "Domínios inválidos"
msgid "Invalid email"
msgstr "E-mail inválido"
#: apps/remix/app/routes/embed+/v1+/authoring+/_layout.tsx
msgid "Invalid embedding presign token provided"
msgstr ""
#: apps/remix/app/components/general/admin-license-card.tsx
msgid "Invalid License Key"
msgstr ""
@@ -5781,6 +5849,8 @@ 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
#: packages/ui/components/pdf-viewer/pdf-viewer-konva-lazy.tsx
#: packages/ui/primitives/pdf-viewer/lazy.tsx
msgid "Loading..."
msgstr "Carregando..."
@@ -6087,6 +6157,10 @@ msgstr "Usuários Ativos Mensais: Usuários que criaram pelo menos um Documento"
msgid "Monthly Active Users: Users that had at least one of their documents completed"
msgstr "Usuários Ativos Mensais: Usuários que tiveram pelo menos um de seus documentos concluídos"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Months"
msgstr ""
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/envelopes-bulk-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
@@ -6208,6 +6282,10 @@ msgstr "Nunca"
msgid "Never expire"
msgstr "Nunca expirar"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Never expires"
msgstr ""
#: apps/remix/app/components/forms/password.tsx
msgid "New Password"
msgstr "Nova Senha"
@@ -6409,6 +6487,10 @@ msgstr ""
msgid "Not found"
msgstr "Não encontrado"
#: apps/remix/app/routes/embed+/_v0+/_layout.tsx
msgid "Not Found"
msgstr ""
#: apps/remix/app/components/forms/signin.tsx
msgid "Not supported"
msgstr "Não suportado"
@@ -6819,6 +6901,7 @@ msgstr "Pagamento em atraso"
msgid "PDF Document"
msgstr "Documento PDF"
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-status.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx
@@ -6934,6 +7017,7 @@ msgid "Please check the CSV file and make sure it is according to our format"
msgstr "Por favor, verifique o arquivo CSV e certifique-se de que está de acordo com nosso formato"
#: apps/remix/app/components/embed/embed-document-waiting-for-turn.tsx
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Please check with the parent application for more information."
msgstr "Por favor, verifique com o aplicativo principal para mais informações."
@@ -7373,6 +7457,10 @@ msgstr ""
msgid "Recipient completed their task"
msgstr ""
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Recipient expired email"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "Recipient failed to validate a 2FA token for the document"
msgstr ""
@@ -7401,6 +7489,11 @@ msgstr ""
msgid "Recipient signing request email"
msgstr "E-mail de solicitação de assinatura do destinatário"
#: packages/lib/utils/document-audit-logs.ts
msgctxt "Audit log format"
msgid "Recipient signing window expired"
msgstr ""
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Recipient updated"
msgstr "Destinatário atualizado"
@@ -7756,6 +7849,7 @@ msgstr "Tentar novamente"
msgid "Return"
msgstr "Retornar"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
msgid "Return Home"
@@ -8164,22 +8258,6 @@ msgstr "Enviar documento"
msgid "Send Document"
msgstr "Enviar Documento"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send document completed email"
msgstr "Enviar e-mail de documento concluído"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send document completed email to the owner"
msgstr "Enviar e-mail de documento concluído para o proprietário"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send document deleted email"
msgstr "Enviar e-mail de documento excluído"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send document pending email"
msgstr "Enviar e-mail de documento pendente"
#: packages/email/templates/confirm-team-email.tsx
msgid "Send documents on behalf of the team using the email address"
msgstr "Enviar documentos em nome da equipe usando o endereço de e-mail"
@@ -8193,16 +8271,8 @@ msgid "Send on Behalf of Team"
msgstr "Enviar em Nome da Equipe"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send recipient removed email"
msgstr "Enviar e-mail de destinatário removido"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send recipient signed email"
msgstr "Enviar e-mail de destinatário assinado"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send recipient signing request email"
msgstr "Enviar e-mail de solicitação de assinatura do destinatário"
msgid "Send recipient expired email to the owner"
msgstr ""
#: apps/remix/app/components/dialogs/document-resend-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx
@@ -8511,6 +8581,10 @@ msgstr "Certificado de assinatura fornecido por"
msgid "Signing Complete!"
msgstr "Assinatura Concluída!"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "Signing Deadline Expired"
msgstr ""
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
msgid "Signing for"
msgstr "Assinando para"
@@ -8535,6 +8609,26 @@ msgstr "Links de assinatura foram gerados para este documento."
msgid "Signing order is enabled."
msgstr "A ordem de assinatura está habilitada."
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Signing Window Expired"
msgstr ""
#. placeholder {0}: recipient.name || recipient.email
#. placeholder {1}: envelope.title
#: packages/lib/jobs/definitions/emails/send-owner-recipient-expired-email.handler.ts
msgid "Signing window expired for \"{0}\" on \"{1}\""
msgstr ""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "Signing window expired for \"{displayName}\" on \"{documentName}\""
msgstr ""
#. placeholder {0}: data.recipientName || data.recipientEmail
#: packages/lib/utils/document-audit-logs.ts
#: packages/lib/utils/document-audit-logs.ts
msgid "Signing window expired for {0}"
msgstr ""
#: apps/remix/app/components/forms/signup.tsx
msgid "Signups are disabled."
msgstr "Inscrições estão desativadas."
@@ -9484,6 +9578,10 @@ msgstr "O e-mail do signatário"
msgid "The signer's name"
msgstr "O nome do signatário"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "The signing deadline for this document has passed. Please contact the document owner if you need a new copy to sign."
msgstr ""
#: apps/remix/app/components/general/avatar-with-recipient.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
@@ -9491,6 +9589,14 @@ msgstr "O nome do signatário"
msgid "The signing link has been copied to your clipboard."
msgstr "O link de assinatura foi copiado para sua área de transferência."
#: packages/email/templates/recipient-expired.tsx
msgid "The signing window for \"{recipientName}\" on document \"{documentName}\" has expired."
msgstr ""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "The signing window for {displayName} on document \"{documentName}\" has expired. You can resend the document to extend their deadline or cancel the document."
msgstr ""
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
msgid "The site banner is a message that is shown at the top of the site. It can be used to display important information to your users."
msgstr "O banner do site é uma mensagem exibida no topo do site. Pode ser usado para exibir informações importantes aos seus usuários."
@@ -9856,6 +9962,10 @@ msgstr "Isso será enviado a todos os destinatários assim que o documento for t
msgid "This will be sent to the document owner once the document has been fully completed."
msgstr "Isso será enviado ao proprietário do documento assim que o documento for totalmente concluído."
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "This will be sent to the document owner when a recipient's signing window has expired."
msgstr ""
#: apps/remix/app/components/tables/organisation-email-domains-table.tsx
msgid "This will check and sync the status of all email domains for this organisation"
msgstr "Isso verificará e sincronizará o status de todos os domínios de e-mail para esta organização"
@@ -10787,6 +10897,7 @@ msgstr "Visualizar documento"
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
#: packages/email/template-components/template-document-invite.tsx
#: packages/email/template-components/template-document-rejected.tsx
#: packages/email/template-components/template-recipient-expired.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
@@ -11314,6 +11425,10 @@ msgstr "URL do Webhook"
msgid "Webhooks"
msgstr "Webhooks"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Weeks"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Welcome"
msgstr "Bem-vindo"
@@ -11399,6 +11514,10 @@ msgstr "Escreva uma descrição para exibir em seu perfil público"
msgid "Yearly"
msgstr "Anual"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Years"
msgstr ""
#: apps/remix/app/components/forms/branding-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
@@ -12373,6 +12492,10 @@ msgstr "Seu código de recuperação foi copiado para sua área de transferênci
msgid "Your recovery codes are listed below. Please store them in a safe place."
msgstr "Seus códigos de recuperação estão listados abaixo. Por favor, guarde-os em um local seguro."
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Your signing window for this document has expired. Please contact the sender for a new invitation."
msgstr ""
#: apps/remix/app/components/forms/support-ticket-form.tsx
msgid "Your support request has been submitted. We'll get back to you soon!"
msgstr "Sua solicitação de suporte foi enviada. Entraremos em contato em breve!"
+123 -1
View File
@@ -2764,6 +2764,10 @@ msgstr "继续查看文档。"
msgid "Continue to login"
msgstr "继续登录"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Controls how long recipients have to complete signing before the document expires. After expiration, recipients can no longer sign the document."
msgstr ""
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Controls the default email settings when new documents or templates are created"
msgstr "控制新建文档或模板时的默认邮件设置"
@@ -3123,6 +3127,10 @@ msgstr "目前仅 Platform 及以上套餐可以配置邮箱域名。"
msgid "Custom {0} MB file"
msgstr "自定义 {0} MB 文件"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Custom duration"
msgstr ""
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups._index.tsx
msgid "Custom Organisation Groups"
msgstr "自定义组织组"
@@ -3165,6 +3173,10 @@ msgstr "日期设置"
msgid "David is the Employee, Lucas is the Manager"
msgstr "David 是员工,Lucas 是经理"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Days"
msgstr ""
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
#: packages/email/templates/organisation-invite.tsx
@@ -3191,6 +3203,10 @@ msgstr "默认邮箱"
msgid "Default Email Settings"
msgstr "默认邮件设置"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Default Envelope Expiration"
msgstr ""
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
msgid "Default file"
msgstr "默认文件"
@@ -3943,6 +3959,7 @@ msgstr "文档"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx
#: apps/remix/app/components/general/skeletons/document-edit-skeleton.tsx
#: apps/remix/app/components/general/user-profile-skeleton.tsx
#: apps/remix/app/components/general/user-profile-timur.tsx
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/tables/organisation-insights-table.tsx
@@ -4731,6 +4748,11 @@ msgstr "所有人都已签署!您将收到一份已签署文档的电子邮件
msgid "Exceeded timeout"
msgstr "超时"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "Expiration"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings._index.tsx
msgid "Expired"
msgstr "已过期"
@@ -4744,6 +4766,11 @@ msgstr "已过期"
msgid "Expires"
msgstr "到期时间"
#. placeholder {0}: recipient.expiresAt ? i18n.date(recipient.expiresAt, DateTime.DATETIME_MED) : 'N/A'
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Expires {0}"
msgstr ""
#. placeholder {0}: DateTime.fromMillis(Math.max(millisecondsRemaining, 0)).toFormat( 'mm:ss', )
#: apps/remix/app/components/general/document-signing/access-auth-2fa-form.tsx
msgid "Expires in {0}"
@@ -5283,6 +5310,10 @@ msgstr "首页(无文件夹)"
msgid "Horizontal"
msgstr "水平"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "我同意将我的账户与此组织关联"
@@ -5385,6 +5416,10 @@ msgstr "在文档中包含审计日志"
msgid "Include the Signing Certificate in the Document"
msgstr "在文档中包含签署证书"
#: apps/remix/app/components/general/billing-plans.tsx
msgid "Includes:"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-information.tsx
#: apps/remix/app/components/general/template/template-page-view-information.tsx
msgid "Information"
@@ -5406,6 +5441,7 @@ msgstr "继承认证方式"
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
#: apps/remix/app/components/forms/email-preferences-form.tsx
msgid "Inherit from organisation"
@@ -5461,6 +5497,10 @@ msgstr "无效的域名"
msgid "Invalid email"
msgstr "邮箱无效"
#: apps/remix/app/routes/embed+/v1+/authoring+/_layout.tsx
msgid "Invalid embedding presign token provided"
msgstr ""
#: apps/remix/app/components/general/admin-license-card.tsx
msgid "Invalid License Key"
msgstr "许可证密钥无效"
@@ -5814,6 +5854,8 @@ msgstr "正在加载建议…"
#: 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
#: packages/ui/components/pdf-viewer/pdf-viewer-konva-lazy.tsx
#: packages/ui/primitives/pdf-viewer/lazy.tsx
msgid "Loading..."
msgstr "正在加载..."
@@ -6120,6 +6162,10 @@ msgstr "月活跃用户:至少创建过一份文档的用户"
msgid "Monthly Active Users: Users that had at least one of their documents completed"
msgstr "月活跃用户:至少有一份文档被完成的用户"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Months"
msgstr ""
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/envelopes-bulk-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
@@ -6241,6 +6287,10 @@ msgstr "从不"
msgid "Never expire"
msgstr "永不过期"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Never expires"
msgstr ""
#: apps/remix/app/components/forms/password.tsx
msgid "New Password"
msgstr "新密码"
@@ -6442,6 +6492,10 @@ msgstr "无(覆盖全局设置)"
msgid "Not found"
msgstr "未找到"
#: apps/remix/app/routes/embed+/_v0+/_layout.tsx
msgid "Not Found"
msgstr ""
#: apps/remix/app/components/forms/signin.tsx
msgid "Not supported"
msgstr "不支持"
@@ -6852,6 +6906,7 @@ msgstr "付款逾期"
msgid "PDF Document"
msgstr "PDF 文档"
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-status.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx
@@ -6967,6 +7022,7 @@ msgid "Please check the CSV file and make sure it is according to our format"
msgstr "请检查 CSV 文件并确保其符合我们的格式要求"
#: apps/remix/app/components/embed/embed-document-waiting-for-turn.tsx
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Please check with the parent application for more information."
msgstr "更多信息请查看父应用程序。"
@@ -7406,6 +7462,10 @@ msgstr "收件人已将此文档加入抄送"
msgid "Recipient completed their task"
msgstr "收件人已完成其任务"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Recipient expired email"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "Recipient failed to validate a 2FA token for the document"
msgstr "收件人未能验证此文档的双重验证 (2FA) 令牌"
@@ -7434,6 +7494,11 @@ msgstr "收件人已签署此文档"
msgid "Recipient signing request email"
msgstr "收件人签署请求邮件"
#: packages/lib/utils/document-audit-logs.ts
msgctxt "Audit log format"
msgid "Recipient signing window expired"
msgstr ""
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Recipient updated"
msgstr "收件人已更新"
@@ -7789,6 +7854,7 @@ msgstr "重试"
msgid "Return"
msgstr "返回"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx
msgid "Return Home"
@@ -8209,6 +8275,10 @@ msgstr "立即将文档发送给收件人"
msgid "Send on Behalf of Team"
msgstr "以团队名义发送"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Send recipient expired email to the owner"
msgstr ""
#: apps/remix/app/components/dialogs/document-resend-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx
msgid "Send reminder"
@@ -8516,6 +8586,10 @@ msgstr "签署证书由以下机构提供"
msgid "Signing Complete!"
msgstr "签署完成!"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "Signing Deadline Expired"
msgstr ""
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
msgid "Signing for"
msgstr "代签对象"
@@ -8540,6 +8614,26 @@ msgstr "已为此文档生成签署链接。"
msgid "Signing order is enabled."
msgstr "签署顺序已启用。"
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Signing Window Expired"
msgstr ""
#. placeholder {0}: recipient.name || recipient.email
#. placeholder {1}: envelope.title
#: packages/lib/jobs/definitions/emails/send-owner-recipient-expired-email.handler.ts
msgid "Signing window expired for \"{0}\" on \"{1}\""
msgstr ""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "Signing window expired for \"{displayName}\" on \"{documentName}\""
msgstr ""
#. placeholder {0}: data.recipientName || data.recipientEmail
#: packages/lib/utils/document-audit-logs.ts
#: packages/lib/utils/document-audit-logs.ts
msgid "Signing window expired for {0}"
msgstr ""
#: apps/remix/app/components/forms/signup.tsx
msgid "Signups are disabled."
msgstr "注册已被禁用。"
@@ -9489,6 +9583,10 @@ msgstr "签署人的邮箱"
msgid "The signer's name"
msgstr "签署人的姓名"
#: apps/remix/app/routes/_recipient+/sign.$token+/expired.tsx
msgid "The signing deadline for this document has passed. Please contact the document owner if you need a new copy to sign."
msgstr ""
#: apps/remix/app/components/general/avatar-with-recipient.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
@@ -9496,6 +9594,14 @@ msgstr "签署人的姓名"
msgid "The signing link has been copied to your clipboard."
msgstr "签署链接已复制到剪贴板。"
#: packages/email/templates/recipient-expired.tsx
msgid "The signing window for \"{recipientName}\" on document \"{documentName}\" has expired."
msgstr ""
#: packages/email/template-components/template-recipient-expired.tsx
msgid "The signing window for {displayName} on document \"{documentName}\" has expired. You can resend the document to extend their deadline or cancel the document."
msgstr ""
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
msgid "The site banner is a message that is shown at the top of the site. It can be used to display important information to your users."
msgstr "站点横幅是在站点顶部显示的一条消息。你可以利用它向用户展示重要信息。"
@@ -9861,6 +9967,10 @@ msgstr "当文档完全完成后,将向所有收件人发送此邮件。"
msgid "This will be sent to the document owner once the document has been fully completed."
msgstr "当文档完全完成后,将向文档所有者发送此邮件。"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "This will be sent to the document owner when a recipient's signing window has expired."
msgstr ""
#: apps/remix/app/components/tables/organisation-email-domains-table.tsx
msgid "This will check and sync the status of all email domains for this organisation"
msgstr "这将检查并同步此组织所有邮箱域名的状态"
@@ -10792,6 +10902,7 @@ msgstr "查看文档"
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
#: packages/email/template-components/template-document-invite.tsx
#: packages/email/template-components/template-document-rejected.tsx
#: packages/email/template-components/template-recipient-expired.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
@@ -11319,6 +11430,10 @@ msgstr "Webhook URL"
msgid "Webhooks"
msgstr "Webhooks"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Weeks"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Welcome"
msgstr "欢迎"
@@ -11404,6 +11519,10 @@ msgstr "撰写将在您的公共主页上展示的简介"
msgid "Yearly"
msgstr "按年"
#: packages/ui/components/document/expiration-period-picker.tsx
msgid "Years"
msgstr ""
#: apps/remix/app/components/forms/branding-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
#: apps/remix/app/components/forms/document-preferences-form.tsx
@@ -12378,6 +12497,10 @@ msgstr "你的恢复代码已复制到剪贴板。"
msgid "Your recovery codes are listed below. Please store them in a safe place."
msgstr "你的恢复代码列在下方。请妥善保存。"
#: apps/remix/app/components/embed/embed-recipient-expired.tsx
msgid "Your signing window for this document has expired. Please contact the sender for a new invitation."
msgstr ""
#: apps/remix/app/components/forms/support-ticket-form.tsx
msgid "Your support request has been submitted. We'll get back to you soon!"
msgstr "您的支持请求已提交。我们会尽快回复您!"
@@ -12446,4 +12569,3 @@ msgstr "您的验证码:"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
msgid "your-domain.com another-domain.com"
msgstr "your-domain.com another-domain.com"
+18
View File
@@ -0,0 +1,18 @@
import { z } from 'zod';
export const ZDocumentDataMetaSchema = z.object({
// Could store other things such as PDF size, etc here.
pages: z
.object({
originalWidth: z.number().describe('Original PDF page width'),
originalHeight: z.number().describe('Original PDF page height'),
scale: z.number().describe('The scale applied to the width/height of the PDF page'),
scaledWidth: z.number().describe('Scaled PDF page image width'),
scaledHeight: z.number().describe('Scaled PDF page image height'),
})
.array(),
});
export type TDocumentDataMeta = z.infer<typeof ZDocumentDataMetaSchema>;
export type DocumentDataVersion = 'initial' | 'current';
+2
View File
@@ -18,6 +18,7 @@ export const ZEmailDomainSchema = EmailDomainSchema.pick({
publicKey: true,
createdAt: true,
updatedAt: true,
lastVerifiedAt: true,
}).extend({
emails: ZOrganisationEmailLiteSchema.array(),
});
@@ -35,6 +36,7 @@ export const ZEmailDomainManySchema = EmailDomainSchema.pick({
selector: true,
createdAt: true,
updatedAt: true,
lastVerifiedAt: true,
});
export type TEmailDomainMany = z.infer<typeof ZEmailDomainManySchema>;
@@ -1,13 +1,19 @@
import { PDF } from '@libpdf/core';
import { DocumentDataType } from '@prisma/client';
import { base64 } from '@scure/base';
import pMap from 'p-map';
import { match } from 'ts-pattern';
import type { TDocumentDataMeta } from '@documenso/lib/types/document-data';
import { ZDocumentDataMetaSchema } from '@documenso/lib/types/document-data';
import { env } from '@documenso/lib/utils/env';
import { prisma } from '@documenso/prisma';
import { AppError } from '../../errors/app-error';
import { pdfToImages } from '../../server-only/ai/pdf-to-images';
import { createDocumentData } from '../../server-only/document-data/create-document-data';
import { normalizePdf } from '../../server-only/pdf/normalize-pdf';
import { getEnvelopeItemPageImageS3Key } from '../../utils/envelope-images';
import { uploadS3File } from './server-actions';
type File = {
@@ -20,7 +26,7 @@ type File = {
* Uploads a document file to the appropriate storage location and creates
* a document data record.
*/
export const putPdfFileServerSide = async (file: File) => {
export const putPdfFileServerSide = async (file: File, initialData?: string) => {
const isEncryptedDocumentsAllowed = false; // Was feature flag.
const arrayBuffer = await file.arrayBuffer();
@@ -41,7 +47,73 @@ export const putPdfFileServerSide = async (file: File) => {
const { type, data } = await putFileServerSide(file);
return await createDocumentData({ type, data });
const newDocumentData = await createDocumentData({ type, data, initialData });
void extractAndStorePdfImages(arrayBuffer, newDocumentData.id).catch((err) => {
console.error(`Error extracting and storing PDF images: ${err}`);
// Do nothing.
});
return newDocumentData;
};
/**
* Extract and stores page images and metadata to S3.
*/
export const extractAndStorePdfImages = async (
arrayBuffer: ArrayBuffer,
documentDataId: string,
): Promise<TDocumentDataMeta['pages']> => {
const images = await pdfToImages(new Uint8Array(arrayBuffer));
const pageMetadata = images.map((image) => ({
originalWidth: image.originalWidth,
originalHeight: image.originalHeight,
scale: image.scale,
scaledWidth: image.scaledWidth,
scaledHeight: image.scaledHeight,
}));
const documentDataMetadata = ZDocumentDataMetaSchema.parse({
pages: pageMetadata,
} satisfies TDocumentDataMeta);
// Only update metadata (page dimensions). Never update type, data, or initialData:
// DocumentData content is immutable so cache keys (documentDataId) stay valid.
const updatedDocumentData = await prisma.documentData.update({
where: { id: documentDataId },
data: {
metadata: documentDataMetadata,
},
});
if (
env('NEXT_PUBLIC_UPLOAD_TRANSPORT') === 's3' &&
updatedDocumentData.type === DocumentDataType.S3_PATH
) {
await pMap(
images,
async (image) => {
const imageBlob = new Blob([new Uint8Array(image.image)], { type: 'image/jpeg' });
const pageIndex = image.pageIndex;
const s3Key = getEnvelopeItemPageImageS3Key(updatedDocumentData.data, pageIndex);
const imageFile = new File([imageBlob], `${pageIndex}.jpeg`, {
type: 'image/jpeg',
});
const { key } = await uploadS3File(imageFile, s3Key);
return key;
},
{ concurrency: 100 },
);
}
return pageMetadata;
};
/**
@@ -63,10 +135,18 @@ export const putNormalizedPdfFileServerSide = async (
arrayBuffer: async () => Promise.resolve(normalized),
});
return await createDocumentData({
const newDocumentData = await createDocumentData({
type: documentData.type,
data: documentData.data,
});
void extractAndStorePdfImages(normalized, newDocumentData.id).catch((err) => {
console.error(`Error extracting and storing PDF images: ${err}`);
// Do nothing.
});
return newDocumentData;
};
/**
@@ -91,13 +91,13 @@ export const getPresignGetUrl = async (key: string) => {
/**
* Uploads a file to S3.
*/
export const uploadS3File = async (file: File) => {
export const uploadS3File = async (file: File, keyOverride?: string) => {
const client = getS3Client();
// Get the basename and extension for the file
const { name, ext } = path.parse(file.name);
const key = `${alphaid(12)}/${slugify(name)}${ext}`;
const key = keyOverride ?? `${alphaid(12)}/${slugify(name)}${ext}`;
const fileBuffer = await file.arrayBuffer();
@@ -124,6 +124,29 @@ export const deleteS3File = async (key: string) => {
);
};
/**
* Be careful about using this function as we don't allow the
* frontend to ever pull a file from S3 directly.
*/
export const UNSAFE_getS3File = async (key: string) => {
// Basic safeguard to prevent path traversal.
// Key should never be user-controlled.
if (key.includes('..') || key.startsWith('/')) {
throw new Error('Invalid S3 key');
}
const client = getS3Client();
const response = await client.send(
new GetObjectCommand({
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
Key: key,
}),
);
return response.Body || null;
};
const getS3Client = () => {
const NEXT_PUBLIC_UPLOAD_TRANSPORT = env('NEXT_PUBLIC_UPLOAD_TRANSPORT');

Some files were not shown because too many files have changed in this diff Show More