mirror of
https://github.com/documenso/documenso.git
synced 2026-06-30 16:20:54 +10:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 18d092f415 | |||
| 6425b242f0 | |||
| 8e8f57661c | |||
| 6f5014a561 | |||
| 653d340668 | |||
| c112392da9 | |||
| bc72d9cb17 | |||
| 9a2f3747db | |||
| 84deea11e4 | |||
| 5fa4c42098 | |||
| 3ad3216c4c | |||
| 36eef79b1a | |||
| 6fb88fede5 | |||
| ab3e8a4074 | |||
| cb6d6e46d0 | |||
| c20affa286 | |||
| a69fe940b5 | |||
| 8186d2817f | |||
| 4fb3c2cb0f |
@@ -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
|
||||
|
||||
+5
-8
@@ -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) =>
|
||||
|
||||
+13
-6
@@ -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>
|
||||
|
||||
+3
-12
@@ -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',
|
||||
{
|
||||
|
||||
+20
-29
@@ -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">
|
||||
|
||||
+16
-28
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
+16
-29
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
+8
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
Generated
+24
-88
@@ -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
@@ -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,
|
||||
|
||||
@@ -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 }>;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
>;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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 "Time‑out 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 CSV‑bestand 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 "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 "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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -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
Reference in New Issue
Block a user