mirror of
https://github.com/documenso/documenso.git
synced 2026-08-23 14:52:23 +10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b0e31e054 | ||
|
|
7964784291 |
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
name: create-justification
|
||||||
|
description: Create a new justification file in .agents/justifications/ with a unique three-word ID, frontmatter, and formatted title
|
||||||
|
license: MIT
|
||||||
|
compatibility: opencode
|
||||||
|
metadata:
|
||||||
|
audience: agents
|
||||||
|
workflow: decision-making
|
||||||
|
---
|
||||||
|
|
||||||
|
## What I do
|
||||||
|
|
||||||
|
I help you create new justification files in the `.agents/justifications/` directory. Each justification file gets:
|
||||||
|
|
||||||
|
- A unique three-word identifier (e.g., `swift-emerald-river`)
|
||||||
|
- Frontmatter with the current date and formatted title
|
||||||
|
- Content you provide
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
Run the script with a slug and content:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tsx scripts/create-justification.ts "decision-name" "Justification content here"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use heredoc for multi-line content:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tsx scripts/create-justification.ts "decision-name" << HEREDOC
|
||||||
|
Multi-line
|
||||||
|
justification content
|
||||||
|
goes here
|
||||||
|
HEREDOC
|
||||||
|
```
|
||||||
|
|
||||||
|
## File format
|
||||||
|
|
||||||
|
Files are created as: `{three-word-id}-{slug}.md`
|
||||||
|
|
||||||
|
Example: `swift-emerald-river-decision-name.md`
|
||||||
|
|
||||||
|
The file includes frontmatter:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
date: 2026-01-13
|
||||||
|
title: Decision Name
|
||||||
|
---
|
||||||
|
|
||||||
|
Your content here
|
||||||
|
```
|
||||||
|
|
||||||
|
## When to use me
|
||||||
|
|
||||||
|
Use this skill when you need to document the reasoning or justification for a decision, approach, or architectural choice. The unique ID ensures no filename conflicts, and the frontmatter provides metadata for organization.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
name: create-plan
|
||||||
|
description: Create a new plan file in .agents/plans/ with a unique three-word ID, frontmatter, and formatted title
|
||||||
|
license: MIT
|
||||||
|
compatibility: opencode
|
||||||
|
metadata:
|
||||||
|
audience: agents
|
||||||
|
workflow: planning
|
||||||
|
---
|
||||||
|
|
||||||
|
## What I do
|
||||||
|
|
||||||
|
I help you create new plan files in the `.agents/plans/` directory. Each plan file gets:
|
||||||
|
|
||||||
|
- A unique three-word identifier (e.g., `happy-blue-moon`)
|
||||||
|
- Frontmatter with the current date and formatted title
|
||||||
|
- Content you provide
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
Run the script with a slug and content:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tsx scripts/create-plan.ts "feature-name" "Plan content here"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use heredoc for multi-line content:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tsx scripts/create-plan.ts "feature-name" << HEREDOC
|
||||||
|
Multi-line
|
||||||
|
plan content
|
||||||
|
goes here
|
||||||
|
HEREDOC
|
||||||
|
```
|
||||||
|
|
||||||
|
## File format
|
||||||
|
|
||||||
|
Files are created as: `{three-word-id}-{slug}.md`
|
||||||
|
|
||||||
|
Example: `happy-blue-moon-feature-name.md`
|
||||||
|
|
||||||
|
The file includes frontmatter:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
date: 2026-01-13
|
||||||
|
title: Feature Name
|
||||||
|
---
|
||||||
|
|
||||||
|
Your content here
|
||||||
|
```
|
||||||
|
|
||||||
|
## When to use me
|
||||||
|
|
||||||
|
Use this skill when you need to create a new plan document for a feature, task, or project. The unique ID ensures no filename conflicts, and the frontmatter provides metadata for organization.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
name: create-scratch
|
||||||
|
description: Create a new scratch file in .agents/scratches/ with a unique three-word ID, frontmatter, and formatted title
|
||||||
|
license: MIT
|
||||||
|
compatibility: opencode
|
||||||
|
metadata:
|
||||||
|
audience: agents
|
||||||
|
workflow: exploration
|
||||||
|
---
|
||||||
|
|
||||||
|
## What I do
|
||||||
|
|
||||||
|
I help you create new scratch files in the `.agents/scratches/` directory. Each scratch file gets:
|
||||||
|
|
||||||
|
- A unique three-word identifier (e.g., `calm-teal-cloud`)
|
||||||
|
- Frontmatter with the current date and formatted title
|
||||||
|
- Content you provide
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
Run the script with a slug and content:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tsx scripts/create-scratch.ts "note-name" "Scratch content here"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use heredoc for multi-line content:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tsx scripts/create-scratch.ts "note-name" << HEREDOC
|
||||||
|
Multi-line
|
||||||
|
scratch content
|
||||||
|
goes here
|
||||||
|
HEREDOC
|
||||||
|
```
|
||||||
|
|
||||||
|
## File format
|
||||||
|
|
||||||
|
Files are created as: `{three-word-id}-{slug}.md`
|
||||||
|
|
||||||
|
Example: `calm-teal-cloud-note-name.md`
|
||||||
|
|
||||||
|
The file includes frontmatter:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
date: 2026-01-13
|
||||||
|
title: Note Name
|
||||||
|
---
|
||||||
|
|
||||||
|
Your content here
|
||||||
|
```
|
||||||
|
|
||||||
|
## When to use me
|
||||||
|
|
||||||
|
Use this skill when you need to create a temporary note, exploration document, or scratch pad for ideas. The unique ID ensures no filename conflicts, and the frontmatter provides metadata for organization.
|
||||||
@@ -80,8 +80,6 @@ NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET=
|
|||||||
NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL=
|
NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL=
|
||||||
# OPTIONAL: Comma-separated list of timestamp authority URLs for PDF signing (enables LTV and archival timestamps).
|
# OPTIONAL: Comma-separated list of timestamp authority URLs for PDF signing (enables LTV and archival timestamps).
|
||||||
NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY=
|
NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY=
|
||||||
# OPTIONAL: Reason to embed in PDF signatures. Defaults to "Signed by Documenso".
|
|
||||||
NEXT_PRIVATE_SIGNING_REASON=
|
|
||||||
# OPTIONAL: Contact info to embed in PDF signatures. Defaults to the webapp URL.
|
# OPTIONAL: Contact info to embed in PDF signatures. Defaults to the webapp URL.
|
||||||
NEXT_PUBLIC_SIGNING_CONTACT_INFO=
|
NEXT_PUBLIC_SIGNING_CONTACT_INFO=
|
||||||
# OPTIONAL: Set to "true" to use the legacy adbe.pkcs7.detached subfilter instead of ETSI.CAdES.detached.
|
# OPTIONAL: Set to "true" to use the legacy adbe.pkcs7.detached subfilter instead of ETSI.CAdES.detached.
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ jobs:
|
|||||||
build_app:
|
build_app:
|
||||||
name: Build App
|
name: Build App
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -33,7 +32,6 @@ jobs:
|
|||||||
build_docker:
|
build_docker:
|
||||||
name: Build Docker Image
|
name: Build Docker Image
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ jobs:
|
|||||||
analyze:
|
analyze:
|
||||||
name: Analyze
|
name: Analyze
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
label-when-assigned:
|
label-when-assigned:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
steps:
|
||||||
- name: Label issue
|
- name: Label issue
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@v6
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
label_issues:
|
label_issues:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
|
||||||
permissions:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v4
|
- uses: actions/labeler@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ jobs:
|
|||||||
build_and_publish_platform_containers:
|
build_and_publish_platform_containers:
|
||||||
name: Build and publish platform containers
|
name: Build and publish platform containers
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 60
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -79,7 +78,6 @@ jobs:
|
|||||||
create_and_publish_manifest:
|
create_and_publish_manifest:
|
||||||
name: Create and publish manifest
|
name: Create and publish manifest
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
|
||||||
needs: build_and_publish_platform_containers
|
needs: build_and_publish_platform_containers
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ jobs:
|
|||||||
validate-pr:
|
validate-pr:
|
||||||
name: Validate PR title
|
name: Validate PR title
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
steps:
|
||||||
- uses: amannn/action-semantic-pull-request@v5
|
- uses: amannn/action-semantic-pull-request@v5
|
||||||
id: lint_pr_title
|
id: lint_pr_title
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
|
||||||
permissions:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ jobs:
|
|||||||
pull_translations:
|
pull_translations:
|
||||||
name: Force pull translations
|
name: Force pull translations
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
|
||||||
environment: Translations
|
environment: Translations
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ jobs:
|
|||||||
pull_translations:
|
pull_translations:
|
||||||
name: Pull translations
|
name: Pull translations
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
|
||||||
environment: Translations
|
environment: Translations
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ jobs:
|
|||||||
extract_translations:
|
extract_translations:
|
||||||
name: Extract and upload translations
|
name: Extract and upload translations
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
|
||||||
environment: Translations
|
environment: Translations
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
legacy-peer-deps = true
|
legacy-peer-deps = true
|
||||||
prefer-dedupe = true
|
prefer-dedupe = true
|
||||||
min-release-age = 7
|
# min-release-age = 7
|
||||||
|
|||||||
@@ -28,62 +28,35 @@ Each document contains one or more PDF files, a list of recipients, and the fiel
|
|||||||
|
|
||||||
A document object contains the following properties:
|
A document object contains the following properties:
|
||||||
|
|
||||||
| Property | Type | Description |
|
| Property | Type | Description |
|
||||||
| ------------------- | -------------- | -------------------------------------------------------------------------------------------- |
|
| --------------- | -------------- | -------------------------------------------------------------- |
|
||||||
| `id` | string | Unique identifier (e.g., `envelope_abc123`) |
|
| `id` | string | Unique identifier (e.g., `envelope_abc123`) |
|
||||||
| `secondaryId` | string | Legacy identifier in prefixed form (`document_123` for documents, `template_123` for templates) |
|
| `type` | string | `DOCUMENT` or `TEMPLATE` |
|
||||||
| `internalVersion` | number | Internal envelope schema version |
|
| `status` | string | Current status: `DRAFT`, `PENDING`, `COMPLETED`, or `REJECTED` |
|
||||||
| `type` | string | `DOCUMENT` or `TEMPLATE` |
|
| `title` | string | Document title |
|
||||||
| `status` | string | Current status: `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED`, or `CANCELLED` |
|
| `source` | string | How the document was created: `DOCUMENT`, `TEMPLATE`, `API` |
|
||||||
| `title` | string | Document title |
|
| `visibility` | string | Who can view: `EVERYONE`, `ADMIN`, `MANAGER_AND_ABOVE` |
|
||||||
| `source` | string | How the document was created: `DOCUMENT`, `TEMPLATE`, `TEMPLATE_DIRECT_LINK` |
|
| `externalId` | string \| null | Your custom identifier for the document |
|
||||||
| `visibility` | string | Who can view: `EVERYONE`, `ADMIN`, `MANAGER_AND_ABOVE` |
|
| `createdAt` | string | ISO 8601 timestamp |
|
||||||
| `templateType` | string | Template visibility: `PUBLIC`, `PRIVATE`, or `ORGANISATION` (only meaningful for templates) |
|
| `updatedAt` | string | ISO 8601 timestamp |
|
||||||
| `externalId` | string \| null | Your custom identifier for the document |
|
| `completedAt` | string \| null | Timestamp when all recipients completed signing |
|
||||||
| `userId` | number | ID of the user who owns the document |
|
| `deletedAt` | string \| null | Timestamp if soft-deleted |
|
||||||
| `teamId` | number | ID of the team the document belongs to |
|
| `recipients` | array | List of recipients and their signing status |
|
||||||
| `folderId` | string \| null | ID of the folder containing the document |
|
| `fields` | array | Signature and form fields on the document |
|
||||||
| `templateId` | number \| null | Legacy ID of the template this document was created from |
|
| `envelopeItems` | array | PDF files attached to the document |
|
||||||
| `authOptions` | object \| null | Access and action authentication requirements |
|
| `documentMeta` | object | Email settings, redirect URL, signing options |
|
||||||
| `formValues` | object \| null | Pre-filled form values |
|
|
||||||
| `publicTitle` | string | Public title shown on profile and direct-link pages |
|
|
||||||
| `publicDescription` | string | Public description shown on profile and direct-link pages |
|
|
||||||
| `createdAt` | string | ISO 8601 timestamp |
|
|
||||||
| `updatedAt` | string | ISO 8601 timestamp |
|
|
||||||
| `completedAt` | string \| null | Timestamp when all recipients completed signing |
|
|
||||||
| `deletedAt` | string \| null | Timestamp if soft-deleted |
|
|
||||||
| `recipients` | array | List of recipients and their signing status |
|
|
||||||
| `fields` | array | Signature and form fields on the document |
|
|
||||||
| `envelopeItems` | array | PDF files attached to the document |
|
|
||||||
| `directLink` | object \| null | Direct-link signing configuration (`id`, `token`, `enabled`, `directTemplateRecipientId`) |
|
|
||||||
| `team` | object | Owning team (`id`, `url`) |
|
|
||||||
| `user` | object | Document owner (`id`, `name`, `email`) |
|
|
||||||
| `documentMeta` | object | Email settings, redirect URL, signing options |
|
|
||||||
|
|
||||||
Documents created through the API have `source: "DOCUMENT"` — there is no separate `API` source value. To tag documents created by your integration, set `externalId` when creating them.
|
|
||||||
|
|
||||||
### Example Document Object
|
### Example Document Object
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": "envelope_abc123xyz",
|
"id": "envelope_abc123xyz",
|
||||||
"secondaryId": "document_123",
|
|
||||||
"internalVersion": 2,
|
|
||||||
"type": "DOCUMENT",
|
"type": "DOCUMENT",
|
||||||
"status": "PENDING",
|
"status": "PENDING",
|
||||||
"source": "DOCUMENT",
|
"source": "API",
|
||||||
"visibility": "EVERYONE",
|
"visibility": "EVERYONE",
|
||||||
"templateType": "PRIVATE",
|
|
||||||
"title": "Service Agreement",
|
"title": "Service Agreement",
|
||||||
"externalId": "contract-2025-001",
|
"externalId": "contract-2025-001",
|
||||||
"userId": 1,
|
|
||||||
"teamId": 1,
|
|
||||||
"folderId": null,
|
|
||||||
"templateId": null,
|
|
||||||
"authOptions": null,
|
|
||||||
"formValues": null,
|
|
||||||
"publicTitle": "",
|
|
||||||
"publicDescription": "",
|
|
||||||
"createdAt": "2025-01-15T10:30:00.000Z",
|
"createdAt": "2025-01-15T10:30:00.000Z",
|
||||||
"updatedAt": "2025-01-15T10:35:00.000Z",
|
"updatedAt": "2025-01-15T10:35:00.000Z",
|
||||||
"completedAt": null,
|
"completedAt": null,
|
||||||
@@ -100,41 +73,23 @@ Documents created through the API have `source: "DOCUMENT"` — there is no sepa
|
|||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"id": 123,
|
"id": "field_123",
|
||||||
"secondaryId": "field_abc123",
|
|
||||||
"type": "SIGNATURE",
|
"type": "SIGNATURE",
|
||||||
"recipientId": 1,
|
|
||||||
"envelopeId": "envelope_abc123xyz",
|
|
||||||
"envelopeItemId": "envelope_item_xyz",
|
|
||||||
"page": 1,
|
"page": 1,
|
||||||
"positionX": "10",
|
"positionX": 10,
|
||||||
"positionY": "80",
|
"positionY": 80,
|
||||||
"width": "30",
|
"width": 30,
|
||||||
"height": "5",
|
"height": 5,
|
||||||
"customText": "",
|
"recipientId": 1
|
||||||
"inserted": false,
|
|
||||||
"fieldMeta": null
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"envelopeItems": [
|
"envelopeItems": [
|
||||||
{
|
{
|
||||||
"id": "envelope_item_xyz",
|
"id": "envelope_item_xyz",
|
||||||
"envelopeId": "envelope_abc123xyz",
|
|
||||||
"documentDataId": "doc_data_abc123",
|
|
||||||
"title": "contract.pdf",
|
"title": "contract.pdf",
|
||||||
"order": 1
|
"order": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"directLink": null,
|
|
||||||
"team": {
|
|
||||||
"id": 1,
|
|
||||||
"url": "your-team"
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "Jane Smith",
|
|
||||||
"email": "jane@example.com"
|
|
||||||
},
|
|
||||||
"documentMeta": {
|
"documentMeta": {
|
||||||
"subject": "Please sign this document",
|
"subject": "Please sign this document",
|
||||||
"message": "Hi, please review and sign this agreement.",
|
"message": "Hi, please review and sign this agreement.",
|
||||||
@@ -144,8 +99,6 @@ Documents created through the API have `source: "DOCUMENT"` — there is no sepa
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Field position and size values are stored as decimals and serialized as strings in API responses.
|
|
||||||
|
|
||||||
## List Documents
|
## List Documents
|
||||||
|
|
||||||
Retrieve a paginated list of documents.
|
Retrieve a paginated list of documents.
|
||||||
@@ -161,7 +114,7 @@ GET /envelope
|
|||||||
| `page` | integer | Page number (default: 1) |
|
| `page` | integer | Page number (default: 1) |
|
||||||
| `perPage` | integer | Results per page (default: 10, max: 100) |
|
| `perPage` | integer | Results per page (default: 10, max: 100) |
|
||||||
| `type` | string | Filter by `DOCUMENT` or `TEMPLATE` |
|
| `type` | string | Filter by `DOCUMENT` or `TEMPLATE` |
|
||||||
| `status` | string | Filter by status: `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED`, `CANCELLED` |
|
| `status` | string | Filter by status: `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED` |
|
||||||
| `source` | string | Filter by creation source |
|
| `source` | string | Filter by creation source |
|
||||||
| `folderId` | string | Filter by folder ID |
|
| `folderId` | string | Filter by folder ID |
|
||||||
| `orderByColumn` | string | Sort field (only `createdAt` supported) |
|
| `orderByColumn` | string | Sort field (only `createdAt` supported) |
|
||||||
@@ -201,8 +154,8 @@ const response = await fetch(`${BASE_URL}/envelope`, {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data, count } = await response.json();
|
const { data, pagination } = await response.json();
|
||||||
console.log(`Found ${count} documents`);
|
console.log(`Found ${pagination.totalItems} documents`);
|
||||||
|
|
||||||
// Filter by status
|
// Filter by status
|
||||||
const pendingResponse = await fetch(
|
const pendingResponse = await fetch(
|
||||||
@@ -244,10 +197,12 @@ const pendingDocs = await pendingResponse.json();
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"count": 42,
|
"pagination": {
|
||||||
"currentPage": 1,
|
"page": 1,
|
||||||
"perPage": 10,
|
"perPage": 10,
|
||||||
"totalPages": 5
|
"totalPages": 5,
|
||||||
|
"totalItems": 42
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -673,72 +628,6 @@ The response includes signing URLs for each recipient:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Cancel Document
|
|
||||||
|
|
||||||
Cancel a pending document. This changes its status from `PENDING` to `CANCELLED`.
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /envelope/cancel
|
|
||||||
```
|
|
||||||
|
|
||||||
### Request Body
|
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
|
||||||
| ------------ | ------ | -------- | ----------------------------------- |
|
|
||||||
| `envelopeId` | string | Yes | Document ID |
|
|
||||||
| `reason` | string | No | Reason for cancelling the document |
|
|
||||||
|
|
||||||
### Code Examples
|
|
||||||
|
|
||||||
<Tabs items={['curl', 'TypeScript']}>
|
|
||||||
<Tab value="curl">
|
|
||||||
```bash
|
|
||||||
curl -X POST "https://app.documenso.com/api/v2/envelope/cancel" \
|
|
||||||
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"envelopeId": "envelope_abc123",
|
|
||||||
"reason": "The agreement is no longer needed."
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
</Tab>
|
|
||||||
<Tab value="TypeScript">
|
|
||||||
```typescript
|
|
||||||
const response = await fetch('https://app.documenso.com/api/v2/envelope/cancel', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
envelopeId: 'envelope_abc123',
|
|
||||||
reason: 'The agreement is no longer needed.',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { success } = await response.json();
|
|
||||||
```
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
### Response
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Behavior
|
|
||||||
|
|
||||||
- Only documents in `PENDING` status can be cancelled. Other statuses return `400`.
|
|
||||||
- Cancellation is not idempotent. Cancelling the same document again returns `400`.
|
|
||||||
- The document owner and team members with `MANAGER` or higher permissions can cancel it. Requests for documents you cannot view return `404`; requests for visible documents without sufficient permissions return `401`.
|
|
||||||
- A successful cancellation fires the `DOCUMENT_CANCELLED` webhook.
|
|
||||||
- Cancellation emails are sent only to eligible non-CC, non-rejected recipients who were sent or opened the document.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Delete Document
|
## Delete Document
|
||||||
|
|
||||||
Delete a document. Completed documents cannot be deleted.
|
Delete a document. Completed documents cannot be deleted.
|
||||||
@@ -781,7 +670,7 @@ const response = await fetch('https://app.documenso.com/api/v2/envelope/delete',
|
|||||||
|
|
||||||
const { success } = await response.json();
|
const { success } = await response.json();
|
||||||
|
|
||||||
```
|
````
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
@@ -791,7 +680,7 @@ const { success } = await response.json();
|
|||||||
{
|
{
|
||||||
"success": true
|
"success": true
|
||||||
}
|
}
|
||||||
```
|
````
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -805,11 +694,9 @@ POST /envelope/get-many
|
|||||||
|
|
||||||
### Request Body
|
### Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
| ---------- | ------ | -------- | ---------------------------------------------------------------------------- |
|
| ------------- | ----- | -------- | --------------------- |
|
||||||
| `ids` | object | Yes | ID selector containing `type` and `ids` |
|
| `envelopeIds` | array | Yes | Array of document IDs |
|
||||||
| `ids.type` | string | Yes | `envelopeId`, `documentId`, or `templateId` |
|
|
||||||
| `ids.ids` | array | Yes | 1-20 IDs: strings for `envelopeId`; numbers for `documentId` or `templateId` |
|
|
||||||
|
|
||||||
### Code Examples
|
### Code Examples
|
||||||
|
|
||||||
@@ -820,17 +707,12 @@ curl -X POST "https://app.documenso.com/api/v2/envelope/get-many" \
|
|||||||
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"ids": {
|
"envelopeIds": ["envelope_abc123", "envelope_def456", "envelope_ghi789"]
|
||||||
"type": "envelopeId",
|
|
||||||
"ids": ["envelope_abc123", "envelope_def456", "envelope_ghi789"]
|
|
||||||
}
|
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab value="TypeScript">
|
<Tab value="TypeScript">
|
||||||
```typescript
|
```typescript
|
||||||
const requestedIds = ['envelope_abc123', 'envelope_def456', 'envelope_ghi789'];
|
|
||||||
|
|
||||||
const response = await fetch('https://app.documenso.com/api/v2/envelope/get-many', {
|
const response = await fetch('https://app.documenso.com/api/v2/envelope/get-many', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -838,36 +720,16 @@ const response = await fetch('https://app.documenso.com/api/v2/envelope/get-many
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
ids: {
|
envelopeIds: ['envelope_abc123', 'envelope_def456', 'envelope_ghi789'],
|
||||||
type: 'envelopeId',
|
|
||||||
ids: requestedIds,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data } = await response.json();
|
const documents = await response.json();
|
||||||
|
|
||||||
```
|
````
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
### Response
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"id": "envelope_abc123",
|
|
||||||
"type": "DOCUMENT",
|
|
||||||
"status": "PENDING",
|
|
||||||
"title": "Service Agreement"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The endpoint silently omits envelopes you cannot access instead of returning `404`. Compare `data.length` with `requestedIds.length` to detect omissions.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Document Statuses
|
## Document Statuses
|
||||||
@@ -878,7 +740,6 @@ The endpoint silently omits envelopes you cannot access instead of returning `40
|
|||||||
| `PENDING` | Document has been sent. Waiting for recipients to sign. |
|
| `PENDING` | Document has been sent. Waiting for recipients to sign. |
|
||||||
| `COMPLETED` | All recipients have signed. Document is sealed. |
|
| `COMPLETED` | All recipients have signed. Document is sealed. |
|
||||||
| `REJECTED` | A recipient rejected the document. |
|
| `REJECTED` | A recipient rejected the document. |
|
||||||
| `CANCELLED` | The document was cancelled by its owner or a team member with `MANAGER` or higher permissions. |
|
|
||||||
|
|
||||||
### Status Transitions
|
### Status Transitions
|
||||||
|
|
||||||
@@ -886,13 +747,11 @@ The endpoint silently omits envelopes you cannot access instead of returning `40
|
|||||||
flowchart LR
|
flowchart LR
|
||||||
DRAFT --> PENDING --> COMPLETED
|
DRAFT --> PENDING --> COMPLETED
|
||||||
PENDING --> REJECTED
|
PENDING --> REJECTED
|
||||||
PENDING --> CANCELLED
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- **DRAFT to PENDING**: Call the distribute endpoint
|
- **DRAFT to PENDING**: Call the distribute endpoint
|
||||||
- **PENDING to COMPLETED**: All recipients complete their signing
|
- **PENDING to COMPLETED**: All recipients complete their signing
|
||||||
- **PENDING to REJECTED**: A recipient rejects the document
|
- **PENDING to REJECTED**: A recipient rejects the document
|
||||||
- **PENDING to CANCELLED**: The document owner or a team member with `MANAGER` or higher permissions cancels the document
|
|
||||||
|
|
||||||
<Callout type="warn">
|
<Callout type="warn">
|
||||||
You cannot modify recipients or fields after a document moves to `PENDING` status.
|
You cannot modify recipients or fields after a document moves to `PENDING` status.
|
||||||
@@ -914,8 +773,8 @@ flowchart LR
|
|||||||
| Parameter | Values | Description |
|
| Parameter | Values | Description |
|
||||||
| ---------- | ------------------------------------------- | ------------------------- |
|
| ---------- | ------------------------------------------- | ------------------------- |
|
||||||
| `type` | `DOCUMENT`, `TEMPLATE` | Filter by envelope type |
|
| `type` | `DOCUMENT`, `TEMPLATE` | Filter by envelope type |
|
||||||
| `status` | `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED`, `CANCELLED` | Filter by status |
|
| `status` | `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED` | Filter by status |
|
||||||
| `source` | `DOCUMENT`, `TEMPLATE`, `TEMPLATE_DIRECT_LINK` | Filter by creation source |
|
| `source` | `DOCUMENT`, `TEMPLATE`, `API` | Filter by creation source |
|
||||||
| `folderId` | string | Filter by folder |
|
| `folderId` | string | Filter by folder |
|
||||||
|
|
||||||
### Sorting
|
### Sorting
|
||||||
@@ -941,10 +800,10 @@ async function getAllPendingDocuments() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data, currentPage, totalPages } = await response.json();
|
const { data, pagination } = await response.json();
|
||||||
documents.push(...data);
|
documents.push(...data);
|
||||||
|
|
||||||
hasMore = currentPage < totalPages;
|
hasMore = page < pagination.totalPages;
|
||||||
page++;
|
page++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ Full reference in the [V2 OpenAPI reference](https://openapi.documenso.com).
|
|||||||
| ------------------------------------------------- | ----------------------------------------------------- |
|
| ------------------------------------------------- | ----------------------------------------------------- |
|
||||||
| `GET /api/v2/document` | `GET /api/v2/envelope` |
|
| `GET /api/v2/document` | `GET /api/v2/envelope` |
|
||||||
| `GET /api/v2/document/{documentId}` | `GET /api/v2/envelope/{envelopeId}` |
|
| `GET /api/v2/document/{documentId}` | `GET /api/v2/envelope/{envelopeId}` |
|
||||||
| `POST /api/v2/document/get-many` | `POST /api/v2/envelope/get-many` (body changes from `documentIds: number[]` to `ids: { type: "documentId"; ids: number[] }`) |
|
| `POST /api/v2/document/get-many` | `POST /api/v2/envelope/get-many` |
|
||||||
| `POST /api/v2/document/create` | `POST /api/v2/envelope/create` |
|
| `POST /api/v2/document/create` | `POST /api/v2/envelope/create` |
|
||||||
| `POST /api/v2/document/create/beta` | `POST /api/v2/envelope/create` |
|
| `POST /api/v2/document/create/beta` | `POST /api/v2/envelope/create` |
|
||||||
| `POST /api/v2/document/update` | `POST /api/v2/envelope/update` |
|
| `POST /api/v2/document/update` | `POST /api/v2/envelope/update` |
|
||||||
@@ -140,7 +140,7 @@ Full reference in the [V2 OpenAPI reference](https://openapi.documenso.com).
|
|||||||
| ------------------------------------- | ------------------------------------------------ |
|
| ------------------------------------- | ------------------------------------------------ |
|
||||||
| `GET /api/v2/template` | `GET /api/v2/envelope` (with `type=TEMPLATE`) |
|
| `GET /api/v2/template` | `GET /api/v2/envelope` (with `type=TEMPLATE`) |
|
||||||
| `GET /api/v2/template/{templateId}` | `GET /api/v2/envelope/{envelopeId}` |
|
| `GET /api/v2/template/{templateId}` | `GET /api/v2/envelope/{envelopeId}` |
|
||||||
| `POST /api/v2/template/get-many` | `POST /api/v2/envelope/get-many` (body changes from `templateIds: number[]` to `ids: { type: "templateId"; ids: number[] }`) |
|
| `POST /api/v2/template/get-many` | `POST /api/v2/envelope/get-many` |
|
||||||
| `POST /api/v2/template/create` | `POST /api/v2/envelope/create` (`type=TEMPLATE`) |
|
| `POST /api/v2/template/create` | `POST /api/v2/envelope/create` (`type=TEMPLATE`) |
|
||||||
| `POST /api/v2/template/create/beta` | `POST /api/v2/envelope/create` (`type=TEMPLATE`) |
|
| `POST /api/v2/template/create/beta` | `POST /api/v2/envelope/create` (`type=TEMPLATE`) |
|
||||||
| `POST /api/v2/template/update` | `POST /api/v2/envelope/update` |
|
| `POST /api/v2/template/update` | `POST /api/v2/envelope/update` |
|
||||||
|
|||||||
@@ -11,12 +11,6 @@ Documenso enforces rate limits on all API endpoints to ensure service stability.
|
|||||||
|
|
||||||
## HTTP Rate Limits
|
## HTTP Rate Limits
|
||||||
|
|
||||||
The rate limit applies to:
|
|
||||||
|
|
||||||
- `/api/v1/*`
|
|
||||||
- `/api/v2/*`
|
|
||||||
- `/api/v2-beta/*`
|
|
||||||
|
|
||||||
**Limit:** 1000 requests per minute per IP address
|
**Limit:** 1000 requests per minute per IP address
|
||||||
**Response:** 429 Too Many Requests
|
**Response:** 429 Too Many Requests
|
||||||
|
|
||||||
@@ -25,7 +19,7 @@ The rate limit applies to:
|
|||||||
this value, in which case you can be rate-limited before reaching the global limit.
|
this value, in which case you can be rate-limited before reaching the global limit.
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
### Global per-IP 429 Response
|
### Rate Limit Response
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -33,22 +27,10 @@ The rate limit applies to:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Rate Limit Headers
|
<Callout type="warn">
|
||||||
|
No rate limit headers are currently provided. When you receive a 429 response, wait at least 60
|
||||||
Responses from `/api/v1/*`, `/api/v2/*`, and `/api/v2-beta/*` include these headers. The only
|
seconds before retrying.
|
||||||
exception is CORS preflight (`OPTIONS`) requests, which are answered before the rate limiter runs
|
</Callout>
|
||||||
and carry no rate limit headers:
|
|
||||||
|
|
||||||
| Header | Description |
|
|
||||||
| ----------------------- | ---------------------------------------------------------------------- |
|
|
||||||
| `X-RateLimit-Limit` | Maximum requests allowed in the current global window |
|
|
||||||
| `X-RateLimit-Remaining` | Requests remaining in the current global window |
|
|
||||||
| `X-RateLimit-Reset` | End of the current global window, as a Unix epoch timestamp in seconds |
|
|
||||||
|
|
||||||
A 429 response from a windowed limiter also includes `Retry-After`, in seconds, with a minimum
|
|
||||||
value of `1`. The global API limit uses fixed, epoch-aligned one-minute buckets, so the actual wait
|
|
||||||
until the next window is between 1 and 60 seconds. Honor `Retry-After` exactly instead of sleeping
|
|
||||||
for a fixed 60 seconds. See the [Retry-After handling example](/docs/developers/examples/common-workflows#error-handling-patterns).
|
|
||||||
|
|
||||||
## Resource Limits
|
## Resource Limits
|
||||||
|
|
||||||
@@ -62,55 +44,24 @@ Beyond HTTP rate limits, your account has usage limits based on your subscriptio
|
|||||||
| Total Recipients | 10 | Unlimited | Unlimited | Unlimited |
|
| Total Recipients | 10 | Unlimited | Unlimited | Unlimited |
|
||||||
| Direct Templates | 3 | Unlimited | Unlimited | Unlimited |
|
| Direct Templates | 3 | Unlimited | Unlimited | Unlimited |
|
||||||
|
|
||||||
### Organisation Limit 429 Responses
|
### Error Response
|
||||||
|
|
||||||
Organisation windowed limits and organisation monthly quotas produce 429 responses whose body
|
When you exceed a resource limit:
|
||||||
shape depends on the API version, and neither matches the global per-IP limiter's
|
|
||||||
`{ "error": "..." }` body.
|
|
||||||
|
|
||||||
On `/api/v1/*`, the body contains only a message:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"message": "Too many requests, please try again later. Contact support if you require higher limits."
|
"error": "You have reached your document limit for this month. Please upgrade your plan.",
|
||||||
|
"code": "LIMIT_EXCEEDED",
|
||||||
|
"statusCode": 400
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
On `/api/v2/*` and `/api/v2-beta/*`, the body is a structured error object:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Too many requests, please try again later. Contact support if you require higher limits.",
|
|
||||||
"code": "TOO_MANY_REQUESTS",
|
|
||||||
"data": {
|
|
||||||
"code": "TOO_MANY_REQUESTS",
|
|
||||||
"httpStatus": 429,
|
|
||||||
"appError": {
|
|
||||||
"code": "TOO_MANY_REQUESTS",
|
|
||||||
"message": "Too many requests, please try again later. Contact support if you require higher limits."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Organisation windowed limit responses include the `X-RateLimit-*` headers and `Retry-After` for
|
|
||||||
their own window. Monthly quota responses carry no quota-specific rate limit headers or
|
|
||||||
`Retry-After` because the quota is not a time window; rely on the status code and message instead.
|
|
||||||
|
|
||||||
## Error Codes
|
## Error Codes
|
||||||
|
|
||||||
| Code | Status | Description |
|
| Code | Status | Description |
|
||||||
| ------------------- | ------ | ------------------------------------------------------------------ |
|
| ------------------- | ------ | ----------------------------- |
|
||||||
| `TOO_MANY_REQUESTS` | 429 | Global per-IP, organisation windowed, or monthly quota exceeded |
|
| `TOO_MANY_REQUESTS` | 429 | HTTP rate limit exceeded |
|
||||||
| `LIMIT_EXCEEDED` | 400 | Resource usage limit exceeded |
|
| `LIMIT_EXCEEDED` | 400 | Resource usage limit exceeded |
|
||||||
|
|
||||||
There are three sources of `TOO_MANY_REQUESTS` responses:
|
|
||||||
|
|
||||||
1. The global per-IP limit, returning the `{ "error": "..." }` body shown above.
|
|
||||||
2. Organisation windowed rate limits for the `api`, `document`, and `email` counters.
|
|
||||||
3. Organisation monthly quotas for the same three counters. Every authenticated API request
|
|
||||||
consumes the `api` counter, so any endpoint can return this 429 once the monthly API quota is
|
|
||||||
exhausted — not just envelope-related ones.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1000,12 +1000,9 @@ async function fetchWithRetry(
|
|||||||
// Retry on rate limit
|
// Retry on rate limit
|
||||||
if (response.status === 429) {
|
if (response.status === 429) {
|
||||||
const retryAfter = response.headers.get('Retry-After');
|
const retryAfter = response.headers.get('Retry-After');
|
||||||
// Honor Retry-After exactly; the cap only applies to the exponential fallback.
|
const delay = retryAfter ? parseInt(retryAfter) * 1000 : baseDelayMs * Math.pow(2, attempt);
|
||||||
const delay = retryAfter
|
|
||||||
? parseInt(retryAfter) * 1000
|
|
||||||
: Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
|
|
||||||
console.log(`Rate limited, waiting ${delay}ms...`);
|
console.log(`Rate limited, waiting ${delay}ms...`);
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
await new Promise((resolve) => setTimeout(resolve, Math.min(delay, maxDelayMs)));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,13 @@
|
|||||||
"postinstall": "fumadocs-mdx"
|
"postinstall": "fumadocs-mdx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fumadocs-core": "16.14.3",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"fumadocs-mdx": "15.2.3",
|
"fumadocs-core": "16.5.0",
|
||||||
"fumadocs-ui": "16.14.3",
|
"fumadocs-mdx": "14.2.6",
|
||||||
|
"fumadocs-ui": "16.5.0",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
"mermaid": "^11.12.2",
|
"mermaid": "^11.12.2",
|
||||||
"next": "16.3.0",
|
"next": "16.2.6",
|
||||||
"next-plausible": "^3.12.5",
|
"next-plausible": "^3.12.5",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
|
|||||||
@@ -12,11 +12,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
"next": "16.3.0"
|
"next": "16.2.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "18.3.27",
|
||||||
"typescript": "5.6.2"
|
"typescript": "5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||||
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
|
||||||
import { ZSignDocumentEmbedDataSchema } from '@documenso/lib/types/embed-document-sign-schema';
|
import { ZSignDocumentEmbedDataSchema } from '@documenso/lib/types/embed-document-sign-schema';
|
||||||
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||||
import { getDocumentDataUrlForPdfViewer } from '@documenso/lib/utils/envelope-download';
|
import { getDocumentDataUrlForPdfViewer } from '@documenso/lib/utils/envelope-download';
|
||||||
@@ -33,7 +32,6 @@ import { useEffect, useId, useLayoutEffect, useMemo, useState } from 'react';
|
|||||||
import { BrandingLogo } from '~/components/general/branding-logo';
|
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||||
import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
|
import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
|
||||||
import { injectCss } from '~/utils/css-vars';
|
import { injectCss } from '~/utils/css-vars';
|
||||||
import { getSigningCompletionErrorMessage } from '~/utils/toast-error-messages';
|
|
||||||
|
|
||||||
import { DocumentSigningAttachmentsPopover } from '../general/document-signing/document-signing-attachments-popover';
|
import { DocumentSigningAttachmentsPopover } from '../general/document-signing/document-signing-attachments-popover';
|
||||||
import { useRequiredDocumentSigningContext } from '../general/document-signing/document-signing-provider';
|
import { useRequiredDocumentSigningContext } from '../general/document-signing/document-signing-provider';
|
||||||
@@ -164,12 +162,9 @@ export const EmbedSignDocumentV1ClientPage = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const error = AppError.parseError(err);
|
|
||||||
const toastMessage = getSigningCompletionErrorMessage(error.code);
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(toastMessage.title),
|
title: _(msg`Something went wrong`),
|
||||||
description: _(toastMessage.description),
|
description: _(msg`We were unable to submit this document at this time. Please try again later.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import { useState } from 'react';
|
|||||||
import { match, P } from 'ts-pattern';
|
import { match, P } from 'ts-pattern';
|
||||||
|
|
||||||
import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
|
import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
|
||||||
import { getSigningCompletionErrorMessage } from '~/utils/toast-error-messages';
|
|
||||||
|
|
||||||
import { useRequiredDocumentSigningContext } from '../../general/document-signing/document-signing-provider';
|
import { useRequiredDocumentSigningContext } from '../../general/document-signing/document-signing-provider';
|
||||||
import { DocumentSigningRejectDialog } from '../../general/document-signing/document-signing-reject-dialog';
|
import { DocumentSigningRejectDialog } from '../../general/document-signing/document-signing-reject-dialog';
|
||||||
@@ -142,12 +141,9 @@ export const MultiSignDocumentSigningView = ({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
onDocumentError?.();
|
onDocumentError?.();
|
||||||
|
|
||||||
const error = AppError.parseError(err);
|
|
||||||
const toastMessage = getSigningCompletionErrorMessage(error.code);
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(toastMessage.title),
|
title: _(msg`Error`),
|
||||||
description: _(toastMessage.description),
|
description: _(msg`Failed to complete the document. Please try again.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { msg } from '@lingui/core/macro';
|
|||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { keepPreviousData } from '@tanstack/react-query';
|
import { keepPreviousData } from '@tanstack/react-query';
|
||||||
import { defaultFilter as commandScore } from 'cmdk';
|
import { commandScore } from 'cmdk/dist/command-score';
|
||||||
import {
|
import {
|
||||||
ArrowLeftIcon,
|
ArrowLeftIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
|
|||||||
+1
-17
@@ -14,7 +14,6 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import type { Field, Recipient } from '@prisma/client';
|
import type { Field, Recipient } from '@prisma/client';
|
||||||
@@ -28,8 +27,6 @@ import { useEmbedSigningContext } from '~/components/embed/embed-signing-context
|
|||||||
import { AccessAuth2FAForm } from '~/components/general/document-signing/access-auth-2fa-form';
|
import { AccessAuth2FAForm } from '~/components/general/document-signing/access-auth-2fa-form';
|
||||||
import { DocumentSigningDisclosure } from '~/components/general/document-signing/document-signing-disclosure';
|
import { DocumentSigningDisclosure } from '~/components/general/document-signing/document-signing-disclosure';
|
||||||
|
|
||||||
import { getSigningCompletionErrorMessage } from '~/utils/toast-error-messages';
|
|
||||||
|
|
||||||
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
|
|
||||||
export type DocumentSigningCompleteDialogProps = {
|
export type DocumentSigningCompleteDialogProps = {
|
||||||
@@ -88,8 +85,7 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
position,
|
position,
|
||||||
disableNameInput = false,
|
disableNameInput = false,
|
||||||
}: DocumentSigningCompleteDialogProps) => {
|
}: DocumentSigningCompleteDialogProps) => {
|
||||||
const { t, i18n } = useLingui();
|
const { t } = useLingui();
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
|
|
||||||
@@ -178,18 +174,6 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This dialog owns the completion error toast for every signing surface
|
|
||||||
// so the user gets a specific, actionable message. Callers should run
|
|
||||||
// their own side effects (e.g. embeds posting document-error) and
|
|
||||||
// rethrow rather than toasting themselves.
|
|
||||||
const toastMessage = getSigningCompletionErrorMessage(err.code);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: i18n._(toastMessage.title),
|
|
||||||
description: i18n._(toastMessage.description),
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
|
||||||
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
|
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
import type { TRecipientAccessAuth } from '@documenso/lib/types/document-auth';
|
import type { TRecipientAccessAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||||
@@ -20,8 +19,6 @@ import { useId, useMemo, useState } from 'react';
|
|||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
import { getSigningCompletionErrorMessage } from '~/utils/toast-error-messages';
|
|
||||||
|
|
||||||
import { AssistantConfirmationDialog, type NextSigner } from '../../dialogs/assistant-confirmation-dialog';
|
import { AssistantConfirmationDialog, type NextSigner } from '../../dialogs/assistant-confirmation-dialog';
|
||||||
import { DocumentSigningCompleteDialog } from './document-signing-complete-dialog';
|
import { DocumentSigningCompleteDialog } from './document-signing-complete-dialog';
|
||||||
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||||
@@ -103,12 +100,9 @@ export const DocumentSigningForm = ({
|
|||||||
try {
|
try {
|
||||||
await completeDocument({ nextSigner });
|
await completeDocument({ nextSigner });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
|
||||||
const toastMessage = getSigningCompletionErrorMessage(error.code);
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(toastMessage.title),
|
title: _(msg`Error`),
|
||||||
description: _(toastMessage.description),
|
description: _(msg`An error occurred while completing the document. Please try again.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -146,17 +146,14 @@ export const DocumentSigningRadioField = ({ field, onSignField, onUnsignField }:
|
|||||||
{isLoading && <DocumentSigningFieldsLoader />}
|
{isLoading && <DocumentSigningFieldsLoader />}
|
||||||
|
|
||||||
{!field.inserted && (
|
{!field.inserted && (
|
||||||
<RadioGroup
|
<RadioGroup onValueChange={(value) => handleSelectItem(value)} className="z-10 my-0.5 gap-y-1">
|
||||||
value={selectedOption}
|
|
||||||
onValueChange={(value) => handleSelectItem(value)}
|
|
||||||
className="z-10 my-0.5 gap-y-1"
|
|
||||||
>
|
|
||||||
{values?.map((item, index) => (
|
{values?.map((item, index) => (
|
||||||
<div key={index} className="flex items-center">
|
<div key={index} className="flex items-center">
|
||||||
<RadioGroupItem
|
<RadioGroupItem
|
||||||
className="h-3 w-3 shrink-0"
|
className="h-3 w-3 shrink-0"
|
||||||
value={item.value}
|
value={item.value}
|
||||||
id={`option-${field.id}-${item.id}`}
|
id={`option-${field.id}-${item.id}`}
|
||||||
|
checked={item.checked}
|
||||||
disabled={isReadOnly}
|
disabled={isReadOnly}
|
||||||
/>
|
/>
|
||||||
{!item.value.includes('empty-value-') && item.value && (
|
{!item.value.includes('empty-value-') && item.value && (
|
||||||
@@ -170,13 +167,14 @@ export const DocumentSigningRadioField = ({ field, onSignField, onUnsignField }:
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{field.inserted && (
|
{field.inserted && (
|
||||||
<RadioGroup value={field.customText ?? ''} className="my-0.5 gap-y-1">
|
<RadioGroup className="my-0.5 gap-y-1">
|
||||||
{values?.map((item, index) => (
|
{values?.map((item, index) => (
|
||||||
<div key={index} className="flex items-center">
|
<div key={index} className="flex items-center">
|
||||||
<RadioGroupItem
|
<RadioGroupItem
|
||||||
className="h-3 w-3"
|
className="h-3 w-3"
|
||||||
value={item.value}
|
value={item.value}
|
||||||
id={`option-${field.id}-${item.id}`}
|
id={`option-${field.id}-${item.id}`}
|
||||||
|
checked={item.value === field.customText}
|
||||||
disabled={isReadOnly}
|
disabled={isReadOnly}
|
||||||
/>
|
/>
|
||||||
{!item.value.includes('empty-value-') && item.value && (
|
{!item.value.includes('empty-value-') && item.value && (
|
||||||
|
|||||||
+11
-4
@@ -148,11 +148,15 @@ export const EnvelopeSignerCompleteDialog = () => {
|
|||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
if (error.code !== AppErrorCode.TWO_FACTOR_AUTH_FAILED) {
|
if (error.code !== AppErrorCode.TWO_FACTOR_AUTH_FAILED) {
|
||||||
|
toast({
|
||||||
|
title: t`Something went wrong`,
|
||||||
|
description: t`We were unable to submit this document at this time. Please try again later.`,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
|
||||||
onDocumentError?.();
|
onDocumentError?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rethrow so DocumentSigningCompleteDialog can handle 2FA retries and
|
|
||||||
// toast a specific completion error message.
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -220,11 +224,14 @@ export const EnvelopeSignerCompleteDialog = () => {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('err', err);
|
console.log('err', err);
|
||||||
|
toast({
|
||||||
|
title: t`Something went wrong`,
|
||||||
|
description: t`We were unable to submit this document at this time. Please try again later.`,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
|
||||||
onDocumentError?.();
|
onDocumentError?.();
|
||||||
|
|
||||||
// Rethrow so DocumentSigningCompleteDialog can toast a specific
|
|
||||||
// completion error message.
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ export default function PDFViewer({
|
|||||||
|
|
||||||
type VirtualizedPageListProps = {
|
type VirtualizedPageListProps = {
|
||||||
scrollParentRef: ScrollTarget;
|
scrollParentRef: ScrollTarget;
|
||||||
constraintRef: React.RefObject<HTMLDivElement | null>;
|
constraintRef: React.RefObject<HTMLDivElement>;
|
||||||
pages: PageMeta[];
|
pages: PageMeta[];
|
||||||
numPages: number;
|
numPages: number;
|
||||||
pdf: pdfjsLib.PDFDocumentProxy;
|
pdf: pdfjsLib.PDFDocumentProxy;
|
||||||
|
|||||||
@@ -31,26 +31,6 @@ function initPosthog() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Surfaces hydration recoveries (React 19 discards the server HTML and
|
|
||||||
* re-renders on the client instead of dying) so we can track how often
|
|
||||||
* extensions/early clicks interfere with hydration in the wild.
|
|
||||||
*/
|
|
||||||
function onRecoverableError(error: unknown, errorInfo: { componentStack?: string }) {
|
|
||||||
console.error('[hydration] recovered from error', error, errorInfo.componentStack);
|
|
||||||
|
|
||||||
if (extractPostHogConfig()) {
|
|
||||||
void import('posthog-js').then(({ default: posthog }) => {
|
|
||||||
if (posthog.__loaded) {
|
|
||||||
posthog.capture('$hydration_recoverable_error', {
|
|
||||||
message: error instanceof Error ? error.message : String(error),
|
|
||||||
componentStack: errorInfo.componentStack,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const locale = detect(fromHtmlTag('lang')) || 'en';
|
const locale = detect(fromHtmlTag('lang')) || 'en';
|
||||||
|
|
||||||
@@ -64,7 +44,6 @@ async function main() {
|
|||||||
<HydratedRouter />
|
<HydratedRouter />
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
{ onRecoverableError },
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -42,51 +42,6 @@ export const getDirectTemplateErrorMessage = (code: string): ToastMessageDescrip
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Toast messages for errors thrown while a recipient attempts to complete
|
|
||||||
* (sign) a document, so the user knows whether retrying can help and what to
|
|
||||||
* do next.
|
|
||||||
*/
|
|
||||||
export const getSigningCompletionErrorMessage = (code: string): ToastMessageDescriptor => {
|
|
||||||
return match(code)
|
|
||||||
.with(AppErrorCode.NOT_FOUND, () => ({
|
|
||||||
title: msg`Document no longer available`,
|
|
||||||
description: msg`This document can no longer be signed. It may have been removed by the sender, or your signing access may have been revoked. Please contact the sender for a new signing link.`,
|
|
||||||
}))
|
|
||||||
.with(AppErrorCode.RECIPIENT_HAS_UNSIGNED_FIELDS, () => ({
|
|
||||||
title: msg`Some fields were not saved`,
|
|
||||||
description: msg`One or more of your required fields have not been saved. Please refresh the page, complete any empty required fields, and try again.`,
|
|
||||||
}))
|
|
||||||
.with(AppErrorCode.RECIPIENT_OUT_OF_TURN, () => ({
|
|
||||||
title: msg`It's not your turn to sign yet`,
|
|
||||||
description: msg`This document is signed in a set order and other recipients must sign before you. You will receive an email when it is your turn.`,
|
|
||||||
}))
|
|
||||||
.with(AppErrorCode.RECIPIENT_EXPIRED, () => ({
|
|
||||||
title: msg`Signing link expired`,
|
|
||||||
description: msg`Your signing link has expired. Please contact the sender to request a new one.`,
|
|
||||||
}))
|
|
||||||
.with(AppErrorCode.ENVELOPE_COMPLETED, () => ({
|
|
||||||
title: msg`Document already completed`,
|
|
||||||
description: msg`This document has already been completed and no further signatures can be added.`,
|
|
||||||
}))
|
|
||||||
.with(AppErrorCode.ENVELOPE_REJECTED, () => ({
|
|
||||||
title: msg`Document rejected`,
|
|
||||||
description: msg`This document has been rejected by a recipient and can no longer be signed.`,
|
|
||||||
}))
|
|
||||||
.with(AppErrorCode.ENVELOPE_CANCELLED, () => ({
|
|
||||||
title: msg`Document cancelled`,
|
|
||||||
description: msg`This document has been cancelled by the sender and can no longer be signed. Please contact the sender if you believe this is a mistake.`,
|
|
||||||
}))
|
|
||||||
.with(AppErrorCode.ENVELOPE_DRAFT, () => ({
|
|
||||||
title: msg`Document not ready`,
|
|
||||||
description: msg`This document has not been sent for signing yet. Please wait for the sender to send it before signing.`,
|
|
||||||
}))
|
|
||||||
.otherwise(() => ({
|
|
||||||
title: msg`Something went wrong`,
|
|
||||||
description: msg`We were unable to submit this document at this time. Please try again later.`,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUploadErrorMessage = (code: string): ToastMessageDescriptor => {
|
export const getUploadErrorMessage = (code: string): ToastMessageDescriptor => {
|
||||||
return match(code)
|
return match(code)
|
||||||
.with(AppErrorCode.TOO_MANY_REQUESTS, () => FAIR_USE_LIMIT_EXCEEDED_ERROR_MESSAGE)
|
.with(AppErrorCode.TOO_MANY_REQUESTS, () => FAIR_USE_LIMIT_EXCEEDED_ERROR_MESSAGE)
|
||||||
|
|||||||
@@ -57,9 +57,9 @@
|
|||||||
"papaparse": "^5.5.3",
|
"papaparse": "^5.5.3",
|
||||||
"posthog-js": "^1.297.2",
|
"posthog-js": "^1.297.2",
|
||||||
"posthog-node": "4.18.0",
|
"posthog-node": "4.18.0",
|
||||||
"react": "^19.2.7",
|
"react": "^18",
|
||||||
"react-call": "^1.8.1",
|
"react-call": "^1.8.1",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^18",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-hook-form": "^7.66.1",
|
"react-hook-form": "^7.66.1",
|
||||||
"react-hotkeys-hook": "^4.6.2",
|
"react-hotkeys-hook": "^4.6.2",
|
||||||
@@ -93,11 +93,11 @@
|
|||||||
"@types/luxon": "^3.7.1",
|
"@types/luxon": "^3.7.1",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/papaparse": "^5.5.0",
|
"@types/papaparse": "^5.5.0",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "18.3.27",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^18",
|
||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"esbuild": "^0.28.1",
|
"esbuild": "^0.27.0",
|
||||||
"remix-flat-routes": "^0.8.5",
|
"remix-flat-routes": "^0.8.5",
|
||||||
"rollup": "^4.53.3",
|
"rollup": "^4.53.3",
|
||||||
"tsx": "^4.23.1",
|
"tsx": "^4.23.1",
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export default defineConfig({
|
|||||||
'nodemailer',
|
'nodemailer',
|
||||||
/playwright/,
|
/playwright/,
|
||||||
'@playwright/browser-chromium',
|
'@playwright/browser-chromium',
|
||||||
'@documenso/skia-canvas',
|
'skia-canvas',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ COPY --from=builder /app/out/json/ .
|
|||||||
COPY --from=builder /app/out/package-lock.json ./package-lock.json
|
COPY --from=builder /app/out/package-lock.json ./package-lock.json
|
||||||
|
|
||||||
COPY --from=builder /app/lingui.config.ts ./lingui.config.ts
|
COPY --from=builder /app/lingui.config.ts ./lingui.config.ts
|
||||||
|
COPY --from=builder /app/patches ./patches
|
||||||
|
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
@@ -114,6 +115,8 @@ WORKDIR /app
|
|||||||
COPY --from=builder --chown=nodejs:nodejs /app/out/json/ .
|
COPY --from=builder --chown=nodejs:nodejs /app/out/json/ .
|
||||||
# Copy the tailwind config files across
|
# Copy the tailwind config files across
|
||||||
COPY --from=builder --chown=nodejs:nodejs /app/out/full/packages/tailwind-config ./packages/tailwind-config
|
COPY --from=builder --chown=nodejs:nodejs /app/out/full/packages/tailwind-config ./packages/tailwind-config
|
||||||
|
# Copy the patches across
|
||||||
|
COPY --from=builder --chown=nodejs:nodejs /app/patches ./patches
|
||||||
|
|
||||||
RUN npm ci --only=production
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
|||||||
Generated
+4789
-3856
File diff suppressed because it is too large
Load Diff
+7
-44
@@ -48,23 +48,20 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.4.8",
|
"@biomejs/biome": "2.4.8",
|
||||||
"@types/react": "^19.2.17",
|
|
||||||
"@types/react-dom": "^19.2.3",
|
|
||||||
"@commitlint/cli": "^20.1.0",
|
"@commitlint/cli": "^20.1.0",
|
||||||
"@commitlint/config-conventional": "^20.0.0",
|
"@commitlint/config-conventional": "^20.0.0",
|
||||||
"@datadog/pprof": "^5.13.5",
|
"@datadog/pprof": "^5.13.5",
|
||||||
"@documenso/skia-canvas": "^3.0.8-documenso.3",
|
|
||||||
"@lingui/cli": "^5.6.0",
|
"@lingui/cli": "^5.6.0",
|
||||||
"@prisma/client": "^6.19.0",
|
"@prisma/client": "^6.19.0",
|
||||||
"@trpc/client": "11.17.0",
|
"@trpc/client": "11.8.1",
|
||||||
"@trpc/react-query": "11.17.0",
|
"@trpc/react-query": "11.8.1",
|
||||||
"@trpc/server": "11.17.0",
|
"@trpc/server": "11.8.1",
|
||||||
"@ts-rest/core": "^3.52.1",
|
"@ts-rest/core": "^3.52.1",
|
||||||
"@ts-rest/open-api": "^3.52.1",
|
"@ts-rest/open-api": "^3.52.1",
|
||||||
"@ts-rest/serverless": "^3.52.1",
|
"@ts-rest/serverless": "^3.52.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"esbuild": "^0.28.1",
|
"esbuild": "^0.27.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"inngest": "^3.54.0",
|
"inngest": "^3.54.0",
|
||||||
"inngest-cli": "^1.17.9",
|
"inngest-cli": "^1.17.9",
|
||||||
@@ -89,61 +86,27 @@
|
|||||||
"zod-prisma-types": "3.3.5"
|
"zod-prisma-types": "3.3.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/google-vertex": "5.0.48",
|
"@ai-sdk/google-vertex": "3.0.81",
|
||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"@libpdf/core": "^0.4.1",
|
"@libpdf/core": "^0.4.1",
|
||||||
"@lingui/conf": "^5.6.0",
|
"@lingui/conf": "^5.6.0",
|
||||||
"@lingui/core": "^5.6.0",
|
"@lingui/core": "^5.6.0",
|
||||||
"@prisma/extension-read-replicas": "^0.4.1",
|
"@prisma/extension-read-replicas": "^0.4.1",
|
||||||
"@radix-ui/react-accordion": "^1.2.16",
|
"ai": "^5.0.104",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.19",
|
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.11",
|
|
||||||
"@radix-ui/react-avatar": "^1.2.2",
|
|
||||||
"@radix-ui/react-checkbox": "^1.3.7",
|
|
||||||
"@radix-ui/react-collapsible": "^1.1.16",
|
|
||||||
"@radix-ui/react-context-menu": "^2.3.3",
|
|
||||||
"@radix-ui/react-dialog": "^1.1.19",
|
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.20",
|
|
||||||
"@radix-ui/react-hover-card": "^1.1.19",
|
|
||||||
"@radix-ui/react-label": "^2.1.11",
|
|
||||||
"@radix-ui/react-menubar": "^1.1.20",
|
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.18",
|
|
||||||
"@radix-ui/react-popover": "^1.1.19",
|
|
||||||
"@radix-ui/react-progress": "^1.1.12",
|
|
||||||
"@radix-ui/react-radio-group": "^1.4.3",
|
|
||||||
"@radix-ui/react-scroll-area": "^1.2.14",
|
|
||||||
"@radix-ui/react-select": "^2.3.3",
|
|
||||||
"@radix-ui/react-separator": "^1.1.11",
|
|
||||||
"@radix-ui/react-slider": "^1.4.3",
|
|
||||||
"@radix-ui/react-slot": "^1.3.0",
|
|
||||||
"@radix-ui/react-switch": "^1.3.3",
|
|
||||||
"@radix-ui/react-tabs": "^1.1.17",
|
|
||||||
"@radix-ui/react-toast": "^1.2.19",
|
|
||||||
"@radix-ui/react-toggle": "^1.1.14",
|
|
||||||
"@radix-ui/react-toggle-group": "^1.1.15",
|
|
||||||
"@radix-ui/react-tooltip": "^1.2.12",
|
|
||||||
"ai": "^7.0.58",
|
|
||||||
"cron-parser": "^5.5.0",
|
"cron-parser": "^5.5.0",
|
||||||
"fflate": "^0.8.3",
|
"fflate": "^0.8.3",
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
"patch-package": "^8.0.1",
|
"patch-package": "^8.0.1",
|
||||||
"posthog-node": "4.18.0",
|
"posthog-node": "4.18.0",
|
||||||
"react": "^19.2.7",
|
"react": "^18",
|
||||||
"react-dom": "^19.2.7",
|
|
||||||
"sharp": "0.35.3",
|
|
||||||
"typescript": "5.6.2",
|
"typescript": "5.6.2",
|
||||||
"@marsidev/react-turnstile": "^1.5.0",
|
"@marsidev/react-turnstile": "^1.5.0",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"lodash": "4.18.1",
|
"lodash": "4.18.1",
|
||||||
"brace-expansion@1": "^1.1.18",
|
|
||||||
"pdfjs-dist": "5.4.296",
|
"pdfjs-dist": "5.4.296",
|
||||||
"postcss": "^8.5.19",
|
"postcss": "^8.5.19",
|
||||||
"react": "$react",
|
|
||||||
"react-dom": "$react-dom",
|
|
||||||
"@types/react": "^19.2.17",
|
|
||||||
"@types/react-dom": "^19.2.3",
|
|
||||||
"typescript": "5.6.2",
|
"typescript": "5.6.2",
|
||||||
"zod": "$zod",
|
"zod": "$zod",
|
||||||
"fumadocs-mdx": {
|
"fumadocs-mdx": {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"arctic": "^3.7.0",
|
"arctic": "^3.7.0",
|
||||||
"hono": "^4.12.14",
|
"hono": "^4.12.14",
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
"react": "^19.2.7",
|
"react": "^18",
|
||||||
"ts-pattern": "^5.9.0",
|
"ts-pattern": "^5.9.0",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
export {
|
export { Body } from '@react-email/body';
|
||||||
Body,
|
export { Button } from '@react-email/button';
|
||||||
Button,
|
export { Column } from '@react-email/column';
|
||||||
Column,
|
export { Container } from '@react-email/container';
|
||||||
Container,
|
export { Font } from '@react-email/font';
|
||||||
Font,
|
export { Head } from '@react-email/head';
|
||||||
Head,
|
export { Heading } from '@react-email/heading';
|
||||||
Heading,
|
export { Hr } from '@react-email/hr';
|
||||||
Hr,
|
export { Html } from '@react-email/html';
|
||||||
Html,
|
export { Img } from '@react-email/img';
|
||||||
Img,
|
export { Link } from '@react-email/link';
|
||||||
Link,
|
export { Preview } from '@react-email/preview';
|
||||||
Preview,
|
export { render } from '@react-email/render';
|
||||||
Row,
|
export { Row } from '@react-email/row';
|
||||||
render,
|
export { Section } from '@react-email/section';
|
||||||
Section,
|
export { Tailwind } from '@react-email/tailwind';
|
||||||
Tailwind,
|
export { Text } from '@react-email/text';
|
||||||
Text,
|
|
||||||
} from 'react-email';
|
|
||||||
|
|||||||
@@ -19,9 +19,27 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/nodemailer-resend": "5.0.0",
|
"@documenso/nodemailer-resend": "5.0.0",
|
||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
"@react-email/render": "2.1.0",
|
"@react-email/body": "0.2.0",
|
||||||
|
"@react-email/button": "0.2.0",
|
||||||
|
"@react-email/code-block": "0.2.0",
|
||||||
|
"@react-email/code-inline": "0.0.5",
|
||||||
|
"@react-email/column": "0.0.13",
|
||||||
|
"@react-email/container": "0.0.15",
|
||||||
|
"@react-email/font": "0.0.9",
|
||||||
|
"@react-email/head": "0.0.12",
|
||||||
|
"@react-email/heading": "0.0.15",
|
||||||
|
"@react-email/hr": "0.0.11",
|
||||||
|
"@react-email/html": "0.0.11",
|
||||||
|
"@react-email/img": "0.0.11",
|
||||||
|
"@react-email/link": "0.0.12",
|
||||||
|
"@react-email/preview": "0.0.13",
|
||||||
|
"@react-email/render": "2.0.0",
|
||||||
|
"@react-email/row": "0.0.12",
|
||||||
|
"@react-email/section": "0.0.16",
|
||||||
|
"@react-email/tailwind": "^2.0.1",
|
||||||
|
"@react-email/text": "0.1.5",
|
||||||
"nodemailer": "^9.0.0",
|
"nodemailer": "^9.0.0",
|
||||||
"react-email": "^6.9.0",
|
"react-email": "^5.0.6",
|
||||||
"resend": "^6.5.2"
|
"resend": "^6.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ type SaveRequest<T, R> = {
|
|||||||
export const useAutoSave = <T, R = void>(onSave: (data: T) => Promise<R>, options: { delay?: number } = {}) => {
|
export const useAutoSave = <T, R = void>(onSave: (data: T) => Promise<R>, options: { delay?: number } = {}) => {
|
||||||
const { delay = 2000 } = options;
|
const { delay = 2000 } = options;
|
||||||
|
|
||||||
const saveTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
const saveTimeoutRef = useRef<NodeJS.Timeout>();
|
||||||
const saveQueueRef = useRef<SaveRequest<T, R>[]>([]);
|
const saveQueueRef = useRef<SaveRequest<T, R>[]>([]);
|
||||||
const isProcessingRef = useRef(false);
|
const isProcessingRef = useRef(false);
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { EnvelopeType, Prisma, ReadStatus, SendStatus, SigningStatus } from '@prisma/client';
|
import { EnvelopeType, Prisma, ReadStatus, SendStatus, SigningStatus } from '@prisma/client';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { createContext, useCallback, useContext, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router';
|
import { useSearchParams } from 'react-router';
|
||||||
|
|
||||||
import type { TDocumentEmailSettings } from '../../types/document-email';
|
import type { TDocumentEmailSettings } from '../../types/document-email';
|
||||||
@@ -107,39 +107,7 @@ export const EnvelopeEditorProvider = ({
|
|||||||
|
|
||||||
const [_searchParams, setSearchParams] = useSearchParams();
|
const [_searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
/**
|
const [envelope, _setEnvelope] = useState(initialEnvelope);
|
||||||
* The envelope is kept in a ref-backed external store instead of useState so
|
|
||||||
* that async consumers (debounced autosave callbacks, flushAutosave, resetForms)
|
|
||||||
* can synchronously read the latest value via `getEnvelope`.
|
|
||||||
*
|
|
||||||
* React subscribes to the store through useSyncExternalStore, keeping renders in
|
|
||||||
* sync without maintaining a separate copy of the state.
|
|
||||||
*/
|
|
||||||
const envelopeStoreRef = useRef(initialEnvelope);
|
|
||||||
const envelopeStoreSubscribersRef = useRef(new Set<() => void>());
|
|
||||||
|
|
||||||
const subscribeToEnvelopeStore = useCallback((onStoreChange: () => void) => {
|
|
||||||
envelopeStoreSubscribersRef.current.add(onStoreChange);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
envelopeStoreSubscribersRef.current.delete(onStoreChange);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getEnvelope = useCallback(() => envelopeStoreRef.current, []);
|
|
||||||
|
|
||||||
const setEnvelope = useCallback((action: React.SetStateAction<TEditorEnvelope>) => {
|
|
||||||
const next = typeof action === 'function' ? action(envelopeStoreRef.current) : action;
|
|
||||||
|
|
||||||
envelopeStoreRef.current = next;
|
|
||||||
|
|
||||||
for (const onStoreChange of envelopeStoreSubscribersRef.current) {
|
|
||||||
onStoreChange();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const envelope = useSyncExternalStore(subscribeToEnvelopeStore, getEnvelope, getEnvelope);
|
|
||||||
|
|
||||||
const [autosaveError, setAutosaveError] = useState<boolean>(false);
|
const [autosaveError, setAutosaveError] = useState<boolean>(false);
|
||||||
|
|
||||||
const isCscMode = IS_INSTANCE_CSC_MODE();
|
const isCscMode = IS_INSTANCE_CSC_MODE();
|
||||||
@@ -167,6 +135,8 @@ export const EnvelopeEditorProvider = ({
|
|||||||
};
|
};
|
||||||
}, [isCscMode, providedEditorConfig]);
|
}, [isCscMode, providedEditorConfig]);
|
||||||
|
|
||||||
|
const envelopeRef = useRef(initialEnvelope);
|
||||||
|
|
||||||
const externalFlushCallbacksRef = useRef<Map<string, () => Promise<void>>>(new Map());
|
const externalFlushCallbacksRef = useRef<Map<string, () => Promise<void>>>(new Map());
|
||||||
const pendingMutationsRef = useRef<Set<Promise<unknown>>>(new Set());
|
const pendingMutationsRef = useRef<Set<Promise<unknown>>>(new Set());
|
||||||
|
|
||||||
@@ -186,6 +156,14 @@ export const EnvelopeEditorProvider = ({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const setEnvelope: typeof _setEnvelope = (action) => {
|
||||||
|
_setEnvelope((prev) => {
|
||||||
|
const next = typeof action === 'function' ? action(prev) : action;
|
||||||
|
envelopeRef.current = next;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const isEmbedded = editorConfig.embedded !== undefined;
|
const isEmbedded = editorConfig.embedded !== undefined;
|
||||||
|
|
||||||
const editorFields = useEditorFields({
|
const editorFields = useEditorFields({
|
||||||
@@ -214,18 +192,16 @@ export const EnvelopeEditorProvider = ({
|
|||||||
try {
|
try {
|
||||||
let recipients: TEditorEnvelope['recipients'] = [];
|
let recipients: TEditorEnvelope['recipients'] = [];
|
||||||
|
|
||||||
const currentEnvelope = getEnvelope();
|
|
||||||
|
|
||||||
if (!isEmbedded) {
|
if (!isEmbedded) {
|
||||||
const response = await setRecipientsMutation.mutateAsync({
|
const response = await setRecipientsMutation.mutateAsync({
|
||||||
envelopeId: currentEnvelope.id,
|
envelopeId: envelope.id,
|
||||||
envelopeType: currentEnvelope.type,
|
envelopeType: envelope.type,
|
||||||
recipients: localRecipients,
|
recipients: localRecipients,
|
||||||
});
|
});
|
||||||
|
|
||||||
recipients = response.data;
|
recipients = response.data;
|
||||||
} else {
|
} else {
|
||||||
recipients = mapLocalRecipientsToRecipients({ envelope: currentEnvelope, localRecipients });
|
recipients = mapLocalRecipientsToRecipients({ envelope, localRecipients });
|
||||||
}
|
}
|
||||||
|
|
||||||
setEnvelope((prev) => ({
|
setEnvelope((prev) => ({
|
||||||
@@ -235,7 +211,9 @@ export const EnvelopeEditorProvider = ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// Reset the local fields to ensure deleted recipient fields are removed.
|
// Reset the local fields to ensure deleted recipient fields are removed.
|
||||||
editorFields.resetForm(getEnvelope().fields);
|
editorFields.resetForm(
|
||||||
|
envelope.fields.filter((field) => recipients.some((recipient) => recipient.id === field.recipientId)),
|
||||||
|
);
|
||||||
|
|
||||||
setAutosaveError(false);
|
setAutosaveError(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -270,18 +248,16 @@ export const EnvelopeEditorProvider = ({
|
|||||||
try {
|
try {
|
||||||
let fields: TSetEnvelopeFieldsResponse['data'] = [];
|
let fields: TSetEnvelopeFieldsResponse['data'] = [];
|
||||||
|
|
||||||
const currentEnvelope = getEnvelope();
|
|
||||||
|
|
||||||
if (!isEmbedded) {
|
if (!isEmbedded) {
|
||||||
const response = await setFieldsMutation.mutateAsync({
|
const response = await setFieldsMutation.mutateAsync({
|
||||||
envelopeId: currentEnvelope.id,
|
envelopeId: envelope.id,
|
||||||
envelopeType: currentEnvelope.type,
|
envelopeType: envelope.type,
|
||||||
fields: localFields,
|
fields: localFields,
|
||||||
});
|
});
|
||||||
|
|
||||||
fields = response.data;
|
fields = response.data;
|
||||||
} else {
|
} else {
|
||||||
fields = mapLocalFieldsToFields({ envelope: currentEnvelope, localFields });
|
fields = mapLocalFieldsToFields({ envelope, localFields });
|
||||||
}
|
}
|
||||||
|
|
||||||
setEnvelope((prev) => ({
|
setEnvelope((prev) => ({
|
||||||
@@ -333,7 +309,7 @@ export const EnvelopeEditorProvider = ({
|
|||||||
try {
|
try {
|
||||||
const response = !isEmbedded
|
const response = !isEmbedded
|
||||||
? await updateEnvelopeMutation.mutateAsync({
|
? await updateEnvelopeMutation.mutateAsync({
|
||||||
envelopeId: getEnvelope().id,
|
envelopeId: envelope.id,
|
||||||
data,
|
data,
|
||||||
meta,
|
meta,
|
||||||
})
|
})
|
||||||
@@ -491,14 +467,12 @@ export const EnvelopeEditorProvider = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resetForms = () => {
|
const resetForms = () => {
|
||||||
const currentEnvelope = getEnvelope();
|
|
||||||
|
|
||||||
editorRecipients.resetForm({
|
editorRecipients.resetForm({
|
||||||
recipients: currentEnvelope.recipients,
|
recipients: envelopeRef.current.recipients,
|
||||||
documentMeta: currentEnvelope.documentMeta,
|
documentMeta: envelopeRef.current.documentMeta,
|
||||||
});
|
});
|
||||||
|
|
||||||
editorFields.resetForm(currentEnvelope.fields);
|
editorFields.resetForm(envelopeRef.current.fields);
|
||||||
};
|
};
|
||||||
|
|
||||||
const flushAutosave = async (): Promise<TEditorEnvelope> => {
|
const flushAutosave = async (): Promise<TEditorEnvelope> => {
|
||||||
@@ -514,7 +488,7 @@ export const EnvelopeEditorProvider = ({
|
|||||||
await Promise.allSettled(Array.from(pendingMutationsRef.current));
|
await Promise.allSettled(Array.from(pendingMutationsRef.current));
|
||||||
}
|
}
|
||||||
|
|
||||||
return getEnvelope();
|
return envelopeRef.current;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
|
|
||||||
import { NEXT_PRIVATE_SIGNING_REASON } from './app';
|
|
||||||
|
|
||||||
describe('NEXT_PRIVATE_SIGNING_REASON', () => {
|
|
||||||
afterEach(() => {
|
|
||||||
vi.unstubAllEnvs();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('defaults to the Documenso signing reason', () => {
|
|
||||||
vi.stubEnv('NEXT_PRIVATE_SIGNING_REASON', undefined);
|
|
||||||
|
|
||||||
expect(NEXT_PRIVATE_SIGNING_REASON()).toBe('Signed by Documenso');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses the configured signing reason verbatim', () => {
|
|
||||||
vi.stubEnv('NEXT_PRIVATE_SIGNING_REASON', 'Signed by Objective');
|
|
||||||
|
|
||||||
expect(NEXT_PRIVATE_SIGNING_REASON()).toBe('Signed by Objective');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -92,7 +92,6 @@ export const IS_AI_FEATURES_CONFIGURED = (): boolean => {
|
|||||||
export const NEXT_PRIVATE_USE_PLAYWRIGHT_PDF = () => env('NEXT_PRIVATE_USE_PLAYWRIGHT_PDF') === 'true';
|
export const NEXT_PRIVATE_USE_PLAYWRIGHT_PDF = () => env('NEXT_PRIVATE_USE_PLAYWRIGHT_PDF') === 'true';
|
||||||
|
|
||||||
export const NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY = () => env('NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY');
|
export const NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY = () => env('NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY');
|
||||||
export const NEXT_PRIVATE_SIGNING_REASON = () => env('NEXT_PRIVATE_SIGNING_REASON') ?? 'Signed by Documenso';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this Documenso instance is running in CSC (Cloud Signature Consortium) mode.
|
* Whether this Documenso instance is running in CSC (Cloud Signature Consortium) mode.
|
||||||
|
|||||||
@@ -43,20 +43,6 @@ export enum AppErrorCode {
|
|||||||
*/
|
*/
|
||||||
RECIPIENT_ALREADY_SIGNED = 'RECIPIENT_ALREADY_SIGNED',
|
RECIPIENT_ALREADY_SIGNED = 'RECIPIENT_ALREADY_SIGNED',
|
||||||
|
|
||||||
/**
|
|
||||||
* A completion request was made for a recipient that still has required
|
|
||||||
* fields which have not been inserted. Usually indicates the client's field
|
|
||||||
* state is out of sync with the server (e.g. a field insert failed to
|
|
||||||
* persist before submission).
|
|
||||||
*/
|
|
||||||
RECIPIENT_HAS_UNSIGNED_FIELDS = 'RECIPIENT_HAS_UNSIGNED_FIELDS',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A completion request was made by a recipient in a sequential signing flow
|
|
||||||
* before the preceding recipients have signed.
|
|
||||||
*/
|
|
||||||
RECIPIENT_OUT_OF_TURN = 'RECIPIENT_OUT_OF_TURN',
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A signer recipient does not have a signature field assigned. Thrown when
|
* A signer recipient does not have a signature field assigned. Thrown when
|
||||||
* distributing an envelope or using a direct template where at least one
|
* distributing an envelope or using a direct template where at least one
|
||||||
@@ -113,8 +99,6 @@ export const genericErrorCodeToTrpcErrorCodeMap: Record<string, { code: string;
|
|||||||
[AppErrorCode.ENVELOPE_LEGACY]: { code: 'BAD_REQUEST', status: 400 },
|
[AppErrorCode.ENVELOPE_LEGACY]: { code: 'BAD_REQUEST', status: 400 },
|
||||||
[AppErrorCode.ENVELOPE_TSP_LOCKED]: { code: 'BAD_REQUEST', status: 400 },
|
[AppErrorCode.ENVELOPE_TSP_LOCKED]: { code: 'BAD_REQUEST', status: 400 },
|
||||||
[AppErrorCode.MISSING_SIGNATURE_FIELD]: { code: 'BAD_REQUEST', status: 400 },
|
[AppErrorCode.MISSING_SIGNATURE_FIELD]: { code: 'BAD_REQUEST', status: 400 },
|
||||||
[AppErrorCode.RECIPIENT_HAS_UNSIGNED_FIELDS]: { code: 'BAD_REQUEST', status: 400 },
|
|
||||||
[AppErrorCode.RECIPIENT_OUT_OF_TURN]: { code: 'BAD_REQUEST', status: 400 },
|
|
||||||
[AppErrorCode.CSC_INSTANCE_MODE_MISMATCH]: { code: 'BAD_REQUEST', status: 400 },
|
[AppErrorCode.CSC_INSTANCE_MODE_MISMATCH]: { code: 'BAD_REQUEST', status: 400 },
|
||||||
[AppErrorCode.CSC_UNLICENSED]: { code: 'FORBIDDEN', status: 403 },
|
[AppErrorCode.CSC_UNLICENSED]: { code: 'FORBIDDEN', status: 403 },
|
||||||
[AppErrorCode.CSC_PROVIDER_INFO_FAILED]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
|
[AppErrorCode.CSC_PROVIDER_INFO_FAILED]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
|
||||||
@@ -323,8 +307,6 @@ export class AppError extends Error {
|
|||||||
AppErrorCode.ENVELOPE_LEGACY,
|
AppErrorCode.ENVELOPE_LEGACY,
|
||||||
AppErrorCode.ENVELOPE_TSP_LOCKED,
|
AppErrorCode.ENVELOPE_TSP_LOCKED,
|
||||||
AppErrorCode.MISSING_SIGNATURE_FIELD,
|
AppErrorCode.MISSING_SIGNATURE_FIELD,
|
||||||
AppErrorCode.RECIPIENT_HAS_UNSIGNED_FIELDS,
|
|
||||||
AppErrorCode.RECIPIENT_OUT_OF_TURN,
|
|
||||||
AppErrorCode.CSC_INSTANCE_MODE_MISMATCH,
|
AppErrorCode.CSC_INSTANCE_MODE_MISMATCH,
|
||||||
AppErrorCode.CSC_CREDENTIAL_LIST_EMPTY,
|
AppErrorCode.CSC_CREDENTIAL_LIST_EMPTY,
|
||||||
AppErrorCode.CSC_CERT_INVALID,
|
AppErrorCode.CSC_CERT_INVALID,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"clean": "rimraf node_modules"
|
"clean": "rimraf node_modules"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/google-vertex": "5.0.48",
|
"@ai-sdk/google-vertex": "3.0.81",
|
||||||
"@aws-sdk/client-s3": "^3.998.0",
|
"@aws-sdk/client-s3": "^3.998.0",
|
||||||
"@aws-sdk/client-sesv2": "^3.998.0",
|
"@aws-sdk/client-sesv2": "^3.998.0",
|
||||||
"@aws-sdk/cloudfront-signer": "^3.998.0",
|
"@aws-sdk/cloudfront-signer": "^3.998.0",
|
||||||
@@ -29,7 +29,6 @@
|
|||||||
"@documenso/email": "*",
|
"@documenso/email": "*",
|
||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"@documenso/signing": "*",
|
"@documenso/signing": "*",
|
||||||
"@documenso/skia-canvas": "^3.0.8-documenso.3",
|
|
||||||
"@lingui/core": "^5.6.0",
|
"@lingui/core": "^5.6.0",
|
||||||
"@lingui/macro": "^5.6.0",
|
"@lingui/macro": "^5.6.0",
|
||||||
"@lingui/react": "^5.6.0",
|
"@lingui/react": "^5.6.0",
|
||||||
@@ -43,7 +42,7 @@
|
|||||||
"@sindresorhus/slugify": "^3.0.0",
|
"@sindresorhus/slugify": "^3.0.0",
|
||||||
"@team-plain/typescript-sdk": "^5.11.0",
|
"@team-plain/typescript-sdk": "^5.11.0",
|
||||||
"@vvo/tzdb": "^6.196.0",
|
"@vvo/tzdb": "^6.196.0",
|
||||||
"ai": "^7.0.58",
|
"ai": "^5.0.104",
|
||||||
"bullmq": "^5.71.1",
|
"bullmq": "^5.71.1",
|
||||||
"colord": "^2.9.3",
|
"colord": "^2.9.3",
|
||||||
"csv-parse": "^6.1.0",
|
"csv-parse": "^6.1.0",
|
||||||
@@ -65,9 +64,10 @@
|
|||||||
"postcss-selector-parser": "^7.1.4",
|
"postcss-selector-parser": "^7.1.4",
|
||||||
"posthog-js": "^1.297.2",
|
"posthog-js": "^1.297.2",
|
||||||
"posthog-node": "4.18.0",
|
"posthog-node": "4.18.0",
|
||||||
"react": "^19.2.7",
|
"react": "^18",
|
||||||
"remeda": "^2.32.0",
|
"remeda": "^2.32.0",
|
||||||
"sharp": "0.35.3",
|
"sharp": "0.34.5",
|
||||||
|
"skia-canvas": "^3.0.8",
|
||||||
"stripe": "^12.18.0",
|
"stripe": "^12.18.0",
|
||||||
"ts-pattern": "^5.9.0",
|
"ts-pattern": "^5.9.0",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Canvas, Image, Path2D } from '@documenso/skia-canvas';
|
|
||||||
import pMap from 'p-map';
|
import pMap from 'p-map';
|
||||||
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
|
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
|
||||||
|
import { Canvas, Image, Path2D } from 'skia-canvas';
|
||||||
|
|
||||||
// @ts-expect-error napi-rs/canvas satisfies the requirements
|
// @ts-expect-error napi-rs/canvas satisfies the requirements
|
||||||
globalThis.Path2D = Path2D;
|
globalThis.Path2D = Path2D;
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export const completeDocumentWithToken = async ({
|
|||||||
nextSigner,
|
nextSigner,
|
||||||
recipientOverride,
|
recipientOverride,
|
||||||
}: CompleteDocumentWithTokenOptions) => {
|
}: CompleteDocumentWithTokenOptions) => {
|
||||||
const envelope = await prisma.envelope.findFirst({
|
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
...unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT),
|
...unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT),
|
||||||
recipients: {
|
recipients: {
|
||||||
@@ -78,23 +78,10 @@ export const completeDocumentWithToken = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// The most common cause is a stale signing page: the document was deleted,
|
|
||||||
// or the recipient was removed, after the link was opened. Surface a
|
|
||||||
// NOT_FOUND instead of leaking a Prisma P2025 as a 500.
|
|
||||||
if (!envelope) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Document not found for the provided signing token',
|
|
||||||
statusCode: 404,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
||||||
|
|
||||||
if (envelope.recipients.length === 0) {
|
if (envelope.recipients.length === 0) {
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
throw new Error(`Document ${envelope.id} has no recipient with token ${token}`);
|
||||||
message: `Document ${envelope.id} has no recipient with the provided token`,
|
|
||||||
statusCode: 404,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [recipient] = envelope.recipients;
|
const [recipient] = envelope.recipients;
|
||||||
@@ -111,19 +98,7 @@ export const completeDocumentWithToken = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (envelope.status !== DocumentStatus.PENDING) {
|
if (envelope.status !== DocumentStatus.PENDING) {
|
||||||
const envelopeStatusErrorCode: Record<DocumentStatus, AppErrorCode> = {
|
throw new Error(`Document ${envelope.id} must be pending`);
|
||||||
[DocumentStatus.DRAFT]: AppErrorCode.ENVELOPE_DRAFT,
|
|
||||||
[DocumentStatus.COMPLETED]: AppErrorCode.ENVELOPE_COMPLETED,
|
|
||||||
[DocumentStatus.REJECTED]: AppErrorCode.ENVELOPE_REJECTED,
|
|
||||||
[DocumentStatus.CANCELLED]: AppErrorCode.ENVELOPE_CANCELLED,
|
|
||||||
// Unreachable: guarded by the status check above.
|
|
||||||
[DocumentStatus.PENDING]: AppErrorCode.INVALID_REQUEST,
|
|
||||||
};
|
|
||||||
|
|
||||||
throw new AppError(envelopeStatusErrorCode[envelope.status], {
|
|
||||||
message: `Document ${envelope.id} must be pending to be completed, found ${envelope.status}`,
|
|
||||||
statusCode: 400,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assertRecipientNotExpired(recipient);
|
assertRecipientNotExpired(recipient);
|
||||||
@@ -141,10 +116,7 @@ export const completeDocumentWithToken = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!isRecipientsTurn) {
|
if (!isRecipientsTurn) {
|
||||||
throw new AppError(AppErrorCode.RECIPIENT_OUT_OF_TURN, {
|
throw new Error(`Recipient ${recipient.id} attempted to complete the document before it was their turn`);
|
||||||
message: `Recipient ${recipient.id} attempted to complete the document before it was their turn`,
|
|
||||||
statusCode: 400,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,10 +279,7 @@ export const completeDocumentWithToken = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fieldsContainUnsignedRequiredField(fields)) {
|
if (fieldsContainUnsignedRequiredField(fields)) {
|
||||||
throw new AppError(AppErrorCode.RECIPIENT_HAS_UNSIGNED_FIELDS, {
|
throw new Error(`Recipient ${recipient.id} has unsigned fields`);
|
||||||
message: `Recipient ${recipient.id} has unsigned fields`,
|
|
||||||
statusCode: 400,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
|
|||||||
@@ -2,9 +2,8 @@
|
|||||||
* !: This is a workaround to fix the memory leak in the skia-canvas library.
|
* !: This is a workaround to fix the memory leak in the skia-canvas library.
|
||||||
* !: Internals are ported from the original `konva/skia-backend.js` file.
|
* !: Internals are ported from the original `konva/skia-backend.js` file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Canvas, DOMMatrix, Image, Path2D } from '@documenso/skia-canvas';
|
|
||||||
import { Konva } from 'konva/lib/_CoreInternals';
|
import { Konva } from 'konva/lib/_CoreInternals';
|
||||||
|
import { Canvas, DOMMatrix, Image, Path2D } from 'skia-canvas';
|
||||||
|
|
||||||
// @ts-expect-error skia-canvas satisfies the requirements
|
// @ts-expect-error skia-canvas satisfies the requirements
|
||||||
global.DOMMatrix = DOMMatrix;
|
global.DOMMatrix = DOMMatrix;
|
||||||
@@ -38,6 +37,6 @@ Konva.Util.createImageElement = () => {
|
|||||||
return node as unknown as HTMLImageElement;
|
return node as unknown as HTMLImageElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
Konva._renderBackend = '@documenso/skia-canvas';
|
Konva._renderBackend = 'skia-canvas';
|
||||||
|
|
||||||
export default Konva;
|
export default Konva;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { FontLibrary } from '@documenso/skia-canvas';
|
|
||||||
import type { Recipient } from '@prisma/client';
|
import type { Recipient } from '@prisma/client';
|
||||||
import { FieldType } from '@prisma/client';
|
import { FieldType } from '@prisma/client';
|
||||||
|
import { FontLibrary } from 'skia-canvas';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
import '../konva/skia-backend';
|
import '../konva/skia-backend';
|
||||||
|
|
||||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
import type { Canvas } from '@documenso/skia-canvas';
|
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
import type { Canvas } from 'skia-canvas';
|
||||||
|
|
||||||
import { renderField } from '../../universal/field-renderer/render-field';
|
import { renderField } from '../../universal/field-renderer/render-field';
|
||||||
import { ensureFontLibrary } from './helpers';
|
import { ensureFontLibrary } from './helpers';
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
// sort-imports-ignore
|
|
||||||
import '../konva/skia-backend';
|
|
||||||
|
|
||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
import type { Canvas } from '@documenso/skia-canvas';
|
|
||||||
import { Image as SkiaImage } from '@documenso/skia-canvas';
|
|
||||||
import type { I18n } from '@lingui/core';
|
import type { I18n } from '@lingui/core';
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import type { DocumentMeta, Envelope, RecipientRole } from '@prisma/client';
|
import type { DocumentMeta, Envelope, RecipientRole } from '@prisma/client';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
import 'konva/skia-backend';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
import type { DateTimeFormatOptions } from 'luxon';
|
import type { DateTimeFormatOptions } from 'luxon';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import type { Canvas } from 'skia-canvas';
|
||||||
|
import { Image as SkiaImage } from 'skia-canvas';
|
||||||
import { match, P } from 'ts-pattern';
|
import { match, P } from 'ts-pattern';
|
||||||
import { UAParser } from 'ua-parser-js';
|
import { UAParser } from 'ua-parser-js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
// sort-imports-ignore
|
|
||||||
import '../konva/skia-backend';
|
|
||||||
|
|
||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
import type { Canvas } from '@documenso/skia-canvas';
|
|
||||||
import { Image as SkiaImage } from '@documenso/skia-canvas';
|
|
||||||
import type { I18n } from '@lingui/core';
|
import type { I18n } from '@lingui/core';
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import type { Field, RecipientRole, Signature } from '@prisma/client';
|
import type { Field, RecipientRole, Signature } from '@prisma/client';
|
||||||
import { SigningStatus } from '@prisma/client';
|
import { SigningStatus } from '@prisma/client';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
import 'konva/skia-backend';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import type { Canvas } from 'skia-canvas';
|
||||||
|
import { Image as SkiaImage } from 'skia-canvas';
|
||||||
import { UAParser } from 'ua-parser-js';
|
import { UAParser } from 'ua-parser-js';
|
||||||
import { renderSVG } from 'uqr';
|
import { renderSVG } from 'uqr';
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ let SkiaImage: any;
|
|||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
const mod = await import('@documenso/skia-canvas');
|
const mod = await import('skia-canvas');
|
||||||
SkiaImage = mod.Image;
|
SkiaImage = mod.Image;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
NEXT_PRIVATE_SIGNING_REASON,
|
|
||||||
NEXT_PRIVATE_USE_LEGACY_SIGNING_SUBFILTER,
|
NEXT_PRIVATE_USE_LEGACY_SIGNING_SUBFILTER,
|
||||||
NEXT_PUBLIC_SIGNING_CONTACT_INFO,
|
NEXT_PUBLIC_SIGNING_CONTACT_INFO,
|
||||||
NEXT_PUBLIC_WEBAPP_URL,
|
NEXT_PUBLIC_WEBAPP_URL,
|
||||||
@@ -43,7 +42,7 @@ export const signPdf = async ({ pdf }: SignOptions) => {
|
|||||||
|
|
||||||
const { bytes } = await pdf.sign({
|
const { bytes } = await pdf.sign({
|
||||||
signer,
|
signer,
|
||||||
reason: NEXT_PRIVATE_SIGNING_REASON(),
|
reason: 'Signed by Documenso',
|
||||||
location: NEXT_PUBLIC_WEBAPP_URL(),
|
location: NEXT_PUBLIC_WEBAPP_URL(),
|
||||||
contactInfo: NEXT_PUBLIC_SIGNING_CONTACT_INFO(),
|
contactInfo: NEXT_PUBLIC_SIGNING_CONTACT_INFO(),
|
||||||
subFilter: NEXT_PRIVATE_USE_LEGACY_SIGNING_SUBFILTER() ? 'adbe.pkcs7.detached' : 'ETSI.CAdES.detached',
|
subFilter: NEXT_PRIVATE_USE_LEGACY_SIGNING_SUBFILTER() ? 'adbe.pkcs7.detached' : 'ETSI.CAdES.detached',
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"@simplewebauthn/server": "^13.2.2",
|
"@simplewebauthn/server": "^13.2.2",
|
||||||
"@tanstack/react-query": "5.90.10",
|
"@tanstack/react-query": "5.90.10",
|
||||||
"@trpc/client": "11.17.0",
|
"@trpc/client": "11.8.1",
|
||||||
"@trpc/react-query": "11.17.0",
|
"@trpc/react-query": "11.8.1",
|
||||||
"@trpc/server": "11.17.0",
|
"@trpc/server": "11.8.1",
|
||||||
"@ts-rest/core": "^3.52.1",
|
"@ts-rest/core": "^3.52.1",
|
||||||
"formidable": "^3.5.4",
|
"formidable": "^3.5.4",
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
|
|||||||
@@ -8,14 +8,13 @@ export const cancelEnvelopeMeta: TrpcRouteMeta = {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/envelope/cancel',
|
path: '/envelope/cancel',
|
||||||
summary: 'Cancel envelope',
|
summary: 'Cancel envelope',
|
||||||
description: 'Cancel a pending envelope',
|
|
||||||
tags: ['Envelope'],
|
tags: ['Envelope'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZCancelEnvelopeRequestSchema = z.object({
|
export const ZCancelEnvelopeRequestSchema = z.object({
|
||||||
envelopeId: z.string().describe('The ID of the envelope to cancel.'),
|
envelopeId: z.string(),
|
||||||
reason: z.string().describe('The reason for cancelling the envelope.').optional(),
|
reason: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZCancelEnvelopeResponseSchema = ZSuccessResponseSchema;
|
export const ZCancelEnvelopeResponseSchema = ZSuccessResponseSchema;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-reques
|
|||||||
import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||||
import { EnvelopeType } from '@prisma/client';
|
import { EnvelopeType } from '@prisma/client';
|
||||||
import type { Logger } from 'pino';
|
import type { Logger } from 'pino';
|
||||||
|
import { match, P } from 'ts-pattern';
|
||||||
|
|
||||||
import { insertFormValuesInPdf } from '../../../lib/server-only/pdf/insert-form-values-in-pdf';
|
import { insertFormValuesInPdf } from '../../../lib/server-only/pdf/insert-form-values-in-pdf';
|
||||||
import { authenticatedProcedure } from '../trpc';
|
import { authenticatedProcedure } from '../trpc';
|
||||||
@@ -148,19 +149,11 @@ export const createEnvelopeRouteCaller = async ({
|
|||||||
accessAuth: recipient.accessAuth,
|
accessAuth: recipient.accessAuth,
|
||||||
actionAuth: recipient.actionAuth,
|
actionAuth: recipient.actionAuth,
|
||||||
fields: recipient.fields?.map((field) => {
|
fields: recipient.fields?.map((field) => {
|
||||||
let documentDataId: string | undefined;
|
const documentDataId = match(field.identifier)
|
||||||
|
.with(P.string, (title) => envelopeItems.find((item) => item.title === title)?.documentDataId)
|
||||||
if (typeof field.identifier === 'string') {
|
.with(P.number, (index) => envelopeItems.at(index)?.documentDataId)
|
||||||
documentDataId = envelopeItems.find((item) => item.title === field.identifier)?.documentDataId;
|
.with(undefined, () => envelopeItems.at(0)?.documentDataId)
|
||||||
}
|
.exhaustive();
|
||||||
|
|
||||||
if (typeof field.identifier === 'number') {
|
|
||||||
documentDataId = envelopeItems.at(field.identifier)?.documentDataId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field.identifier === undefined) {
|
|
||||||
documentDataId = envelopeItems.at(0)?.documentDataId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!documentDataId) {
|
if (!documentDataId) {
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||||
|
|||||||
@@ -8,13 +8,12 @@ export const deleteEnvelopeMeta: TrpcRouteMeta = {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/envelope/delete',
|
path: '/envelope/delete',
|
||||||
summary: 'Delete envelope',
|
summary: 'Delete envelope',
|
||||||
description: 'Delete an envelope',
|
|
||||||
tags: ['Envelope'],
|
tags: ['Envelope'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZDeleteEnvelopeRequestSchema = z.object({
|
export const ZDeleteEnvelopeRequestSchema = z.object({
|
||||||
envelopeId: z.string().describe('The ID of the envelope to delete.'),
|
envelopeId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZDeleteEnvelopeResponseSchema = ZSuccessResponseSchema;
|
export const ZDeleteEnvelopeResponseSchema = ZSuccessResponseSchema;
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ export const signEnvelopeFieldRoute = procedure
|
|||||||
const field = await prisma.field.findFirst({
|
const field = await prisma.field.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: fieldId,
|
id: fieldId,
|
||||||
recipient: {
|
recipient:
|
||||||
...(recipient.role === RecipientRole.ASSISTANT
|
recipient.role === RecipientRole.ASSISTANT
|
||||||
? {
|
? {
|
||||||
signingStatus: {
|
signingStatus: {
|
||||||
not: SigningStatus.SIGNED,
|
not: SigningStatus.SIGNED,
|
||||||
@@ -52,8 +52,7 @@ export const signEnvelopeFieldRoute = procedure
|
|||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
id: recipient.id,
|
id: recipient.id,
|
||||||
}),
|
},
|
||||||
},
|
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
envelope: {
|
envelope: {
|
||||||
|
|||||||
@@ -12,32 +12,24 @@ export const updateEnvelopeMeta: TrpcRouteMeta = {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/envelope/update',
|
path: '/envelope/update',
|
||||||
summary: 'Update envelope',
|
summary: 'Update envelope',
|
||||||
description: 'Update envelope properties and settings',
|
|
||||||
tags: ['Envelope'],
|
tags: ['Envelope'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZUpdateEnvelopeRequestSchema = z.object({
|
export const ZUpdateEnvelopeRequestSchema = z.object({
|
||||||
envelopeId: z.string().describe('The ID of the envelope to update.'),
|
envelopeId: z.string(),
|
||||||
data: z
|
data: z
|
||||||
.object({
|
.object({
|
||||||
title: ZDocumentTitleSchema.optional(),
|
title: ZDocumentTitleSchema.optional(),
|
||||||
externalId: ZDocumentExternalIdSchema.nullish(),
|
externalId: ZDocumentExternalIdSchema.nullish(),
|
||||||
visibility: ZDocumentVisibilitySchema.optional(),
|
visibility: ZDocumentVisibilitySchema.optional(),
|
||||||
globalAccessAuth: z
|
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
||||||
.array(ZDocumentAccessAuthTypesSchema)
|
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
|
||||||
.describe('The authentication methods required to access the envelope.')
|
folderId: z.string().nullish(),
|
||||||
.optional(),
|
templateType: z.nativeEnum(TemplateType).optional(),
|
||||||
globalActionAuth: z
|
|
||||||
.array(ZDocumentActionAuthTypesSchema)
|
|
||||||
.describe('The authentication methods required to sign the envelope.')
|
|
||||||
.optional(),
|
|
||||||
folderId: z.string().describe('The ID of the folder containing the envelope.').nullish(),
|
|
||||||
templateType: z.nativeEnum(TemplateType).describe('The template type.').optional(),
|
|
||||||
})
|
})
|
||||||
.describe('The envelope properties to update.')
|
|
||||||
.optional(),
|
.optional(),
|
||||||
meta: ZDocumentMetaUpdateSchema.describe('The email and signing settings to update.').optional(),
|
meta: ZDocumentMetaUpdateSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZUpdateEnvelopeResponseSchema = ZEnvelopeLiteSchema;
|
export const ZUpdateEnvelopeResponseSchema = ZEnvelopeLiteSchema;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/
|
|||||||
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||||
import { formatSigningLink } from '@documenso/lib/utils/recipients';
|
import { formatSigningLink } from '@documenso/lib/utils/recipients';
|
||||||
import { EnvelopeType } from '@prisma/client';
|
import { EnvelopeType } from '@prisma/client';
|
||||||
|
import { match, P } from 'ts-pattern';
|
||||||
|
|
||||||
import { authenticatedProcedure } from '../trpc';
|
import { authenticatedProcedure } from '../trpc';
|
||||||
import { useEnvelopeMeta, ZUseEnvelopeRequestSchema, ZUseEnvelopeResponseSchema } from './use-envelope.types';
|
import { useEnvelopeMeta, ZUseEnvelopeRequestSchema, ZUseEnvelopeResponseSchema } from './use-envelope.types';
|
||||||
@@ -87,20 +88,11 @@ export const useEnvelopeRoute = authenticatedProcedure
|
|||||||
|
|
||||||
// Map custom document data using identifiers
|
// Map custom document data using identifiers
|
||||||
const customDocumentDataMapped = customDocumentData?.map((mapping) => {
|
const customDocumentDataMapped = customDocumentData?.map((mapping) => {
|
||||||
let documentDataId: string | undefined;
|
|
||||||
|
|
||||||
// Find the uploaded file by identifier
|
// Find the uploaded file by identifier
|
||||||
if (typeof mapping.identifier === 'string') {
|
const documentDataId = match(mapping.identifier)
|
||||||
documentDataId = uploadedFiles.find((file) => file.name === mapping.identifier)?.documentDataId;
|
.with(P.string, (name) => uploadedFiles.find((file) => file.name === name)?.documentDataId)
|
||||||
}
|
.with(P.number, (index) => uploadedFiles.at(index)?.documentDataId)
|
||||||
|
.exhaustive();
|
||||||
if (typeof mapping.identifier === 'number') {
|
|
||||||
documentDataId = uploadedFiles.at(mapping.identifier)?.documentDataId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mapping.identifier === undefined) {
|
|
||||||
documentDataId = uploadedFiles.at(0)?.documentDataId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!documentDataId) {
|
if (!documentDataId) {
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||||
|
|||||||
@@ -603,7 +603,7 @@ export const recipientRouter = router({
|
|||||||
// can't complete via this route — they go through the CSC sync sign
|
// can't complete via this route — they go through the CSC sync sign
|
||||||
// flow (`enterprise.csc.signEnvelope`). This route returns the redirect URL
|
// flow (`enterprise.csc.signEnvelope`). This route returns the redirect URL
|
||||||
// for the credential-scope OAuth round-trip.
|
// for the credential-scope OAuth round-trip.
|
||||||
const envelope = await prisma.envelope.findFirst({
|
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
...unsafeBuildEnvelopeIdQuery({ type: 'documentId', id: documentId }, EnvelopeType.DOCUMENT),
|
...unsafeBuildEnvelopeIdQuery({ type: 'documentId', id: documentId }, EnvelopeType.DOCUMENT),
|
||||||
recipients: { some: { token } },
|
recipients: { some: { token } },
|
||||||
@@ -611,16 +611,6 @@ export const recipientRouter = router({
|
|||||||
select: { signatureLevel: true, internalVersion: true },
|
select: { signatureLevel: true, internalVersion: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
// The most common cause is a stale signing page: the document was
|
|
||||||
// deleted, or the recipient was removed, after the link was opened.
|
|
||||||
// Surface a NOT_FOUND instead of leaking a Prisma P2025 as a 500.
|
|
||||||
if (!envelope) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Document not found for the provided signing token',
|
|
||||||
statusCode: 404,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isTspEnvelope(envelope)) {
|
if (isTspEnvelope(envelope)) {
|
||||||
return await prepareCscRecipientSigning({
|
return await prepareCscRecipientSigning({
|
||||||
recipientToken: token,
|
recipientToken: token,
|
||||||
|
|||||||
Vendored
-1
@@ -59,7 +59,6 @@ declare namespace NodeJS {
|
|||||||
NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET?: string;
|
NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET?: string;
|
||||||
NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL?: 'AES' | 'QES';
|
NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL?: 'AES' | 'QES';
|
||||||
NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY?: string;
|
NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY?: string;
|
||||||
NEXT_PRIVATE_SIGNING_REASON?: string;
|
|
||||||
NEXT_PUBLIC_SIGNING_CONTACT_INFO?: string;
|
NEXT_PUBLIC_SIGNING_CONTACT_INFO?: string;
|
||||||
NEXT_PRIVATE_USE_LEGACY_SIGNING_SUBFILTER?: string;
|
NEXT_PRIVATE_USE_LEGACY_SIGNING_SUBFILTER?: string;
|
||||||
|
|
||||||
|
|||||||
@@ -2,27 +2,16 @@ import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-c
|
|||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import type { Field, Recipient } from '@prisma/client';
|
import type { Field, Recipient } from '@prisma/client';
|
||||||
import { FieldType, SigningStatus } from '@prisma/client';
|
import { SigningStatus } from '@prisma/client';
|
||||||
import {
|
import { ClockIcon, EyeOffIcon, LockIcon } from 'lucide-react';
|
||||||
CalendarDaysIcon,
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
CheckSquareIcon,
|
|
||||||
ChevronDownIcon,
|
|
||||||
ContactIcon,
|
|
||||||
DiscIcon,
|
|
||||||
EyeOffIcon,
|
|
||||||
HashIcon,
|
|
||||||
LockIcon,
|
|
||||||
MailIcon,
|
|
||||||
TypeIcon,
|
|
||||||
UserIcon,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { type ElementType, useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { isTemplateRecipientEmailPlaceholder } from '../../../lib/constants/template';
|
import { isTemplateRecipientEmailPlaceholder } from '../../../lib/constants/template';
|
||||||
import { extractInitials } from '../../../lib/utils/recipient-formatter';
|
import { extractInitials } from '../../../lib/utils/recipient-formatter';
|
||||||
import { SignatureIcon } from '../../icons/signature';
|
import { SignatureIcon } from '../../icons/signature';
|
||||||
import { cn } from '../../lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
import { Avatar, AvatarFallback } from '../../primitives/avatar';
|
import { Avatar, AvatarFallback } from '../../primitives/avatar';
|
||||||
|
import { Badge } from '../../primitives/badge';
|
||||||
import { FRIENDLY_FIELD_TYPE } from '../../primitives/document-flow/types';
|
import { FRIENDLY_FIELD_TYPE } from '../../primitives/document-flow/types';
|
||||||
import { PopoverHover } from '../../primitives/popover';
|
import { PopoverHover } from '../../primitives/popover';
|
||||||
|
|
||||||
@@ -38,18 +27,16 @@ interface EnvelopeRecipientFieldTooltipProps {
|
|||||||
showRecipientColors?: boolean;
|
showRecipientColors?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FIELD_TYPE_ICONS: Record<FieldType, ElementType> = {
|
const getRecipientDisplayText = (recipient: { name: string; email: string }) => {
|
||||||
[FieldType.SIGNATURE]: SignatureIcon,
|
if (recipient.name && !isTemplateRecipientEmailPlaceholder(recipient.email)) {
|
||||||
[FieldType.FREE_SIGNATURE]: SignatureIcon,
|
return `${recipient.name} (${recipient.email})`;
|
||||||
[FieldType.INITIALS]: ContactIcon,
|
}
|
||||||
[FieldType.TEXT]: TypeIcon,
|
|
||||||
[FieldType.DATE]: CalendarDaysIcon,
|
if (recipient.name && isTemplateRecipientEmailPlaceholder(recipient.email)) {
|
||||||
[FieldType.EMAIL]: MailIcon,
|
return recipient.name;
|
||||||
[FieldType.NAME]: UserIcon,
|
}
|
||||||
[FieldType.NUMBER]: HashIcon,
|
|
||||||
[FieldType.RADIO]: DiscIcon,
|
return recipient.email;
|
||||||
[FieldType.CHECKBOX]: CheckSquareIcon,
|
|
||||||
[FieldType.DROPDOWN]: ChevronDownIcon,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,8 +50,6 @@ export function EnvelopeRecipientFieldTooltip({
|
|||||||
}: EnvelopeRecipientFieldTooltipProps) {
|
}: EnvelopeRecipientFieldTooltipProps) {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
const FieldIcon = FIELD_TYPE_ICONS[field.type];
|
|
||||||
|
|
||||||
const [hideField, setHideField] = useState<boolean>(!showRecipientTooltip);
|
const [hideField, setHideField] = useState<boolean>(!showRecipientTooltip);
|
||||||
|
|
||||||
const [coords, setCoords] = useState({
|
const [coords, setCoords] = useState({
|
||||||
@@ -153,64 +138,54 @@ export function EnvelopeRecipientFieldTooltip({
|
|||||||
</Avatar>
|
</Avatar>
|
||||||
}
|
}
|
||||||
contentProps={{
|
contentProps={{
|
||||||
className: 'flex w-64 flex-col overflow-hidden p-0 text-sm',
|
className: 'relative flex mb-4 w-fit flex-col p-4 text-sm',
|
||||||
sideOffset: 20,
|
|
||||||
onOpenAutoFocus: (event) => event.preventDefault(),
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 p-3">
|
{showFieldStatus && (
|
||||||
<FieldIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
|
<Badge
|
||||||
|
className="mx-auto mb-1 py-0.5"
|
||||||
<p className="min-w-0 flex-1 truncate font-medium">
|
variant={
|
||||||
<Trans>{t(FRIENDLY_FIELD_TYPE[field.type])} field</Trans>
|
field?.fieldMeta?.readOnly
|
||||||
</p>
|
? 'neutral'
|
||||||
|
: field.recipient.signingStatus === SigningStatus.SIGNED
|
||||||
{showFieldStatus && (
|
? 'default'
|
||||||
<div className="flex shrink-0 items-center gap-1.5 text-xs">
|
: 'secondary'
|
||||||
{field?.fieldMeta?.readOnly ? (
|
}
|
||||||
<>
|
|
||||||
<LockIcon className="h-3 w-3 text-muted-foreground" />
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
<Trans>Read Only</Trans>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : field.recipient.signingStatus === SigningStatus.SIGNED ? (
|
|
||||||
<>
|
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-green-500" />
|
|
||||||
<span className="text-green-600 dark:text-green-400">
|
|
||||||
<Trans>Signed</Trans>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-amber-400" />
|
|
||||||
<span className="text-amber-600 dark:text-amber-400">
|
|
||||||
<Trans>Pending</Trans>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3 border-border/50 border-t bg-muted/50 px-3 py-2.5">
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="truncate font-medium text-xs">{field.recipient.name || field.recipient.email}</p>
|
|
||||||
|
|
||||||
{!isTemplateRecipientEmailPlaceholder(field.recipient.email) && field.recipient.name && (
|
|
||||||
<p className="truncate text-muted-foreground text-xs">{field.recipient.email}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="-m-1 shrink-0 rounded-sm p-1 text-muted-foreground hover:bg-background hover:text-foreground"
|
|
||||||
onClick={() => setHideField(true)}
|
|
||||||
title={t`Hide field`}
|
|
||||||
>
|
>
|
||||||
<EyeOffIcon className="h-3.5 w-3.5" />
|
{field?.fieldMeta?.readOnly ? (
|
||||||
</button>
|
<>
|
||||||
</div>
|
<LockIcon className="mr-1 h-3 w-3" />
|
||||||
|
<Trans>Read Only</Trans>
|
||||||
|
</>
|
||||||
|
) : field.recipient.signingStatus === SigningStatus.SIGNED ? (
|
||||||
|
<>
|
||||||
|
<SignatureIcon className="mr-1 h-3 w-3" />
|
||||||
|
<Trans>Signed</Trans>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ClockIcon className="mr-1 h-3 w-3" />
|
||||||
|
<Trans>Pending</Trans>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="text-center font-semibold">
|
||||||
|
<span>
|
||||||
|
<Trans>{t(FRIENDLY_FIELD_TYPE[field.type])} field</Trans>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-1 text-center text-muted-foreground text-xs">{getRecipientDisplayText(field.recipient)}</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="absolute top-0 right-0 my-1 p-2 focus:outline-none focus-visible:ring-0"
|
||||||
|
onClick={() => setHideField(true)}
|
||||||
|
title="Hide field"
|
||||||
|
>
|
||||||
|
<EyeOffIcon className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
</PopoverHover>
|
</PopoverHover>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const SigningCard3D = ({ className, name, signature, signingCelebrationIm
|
|||||||
|
|
||||||
const [trackMouse, setTrackMouse] = useState(false);
|
const [trackMouse, setTrackMouse] = useState(false);
|
||||||
|
|
||||||
const timeoutRef = useRef<number | undefined>(undefined);
|
const timeoutRef = useRef<number | undefined>();
|
||||||
|
|
||||||
const cardX = useMotionValue(0);
|
const cardX = useMotionValue(0);
|
||||||
const cardY = useMotionValue(0);
|
const cardY = useMotionValue(0);
|
||||||
|
|||||||
@@ -18,32 +18,59 @@
|
|||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
"@documenso/tsconfig": "*",
|
"@documenso/tsconfig": "*",
|
||||||
"@types/luxon": "^3.7.1",
|
"@types/luxon": "^3.7.1",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "18.3.27",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^18",
|
||||||
"react": "^19.2.7",
|
"react": "^18",
|
||||||
"typescript": "5.6.2"
|
"typescript": "5.6.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/lib": "*",
|
"@documenso/lib": "*",
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hello-pangea/dnd": "^16.6.0",
|
||||||
"@hookform/resolvers": "^3",
|
"@hookform/resolvers": "^3",
|
||||||
"@lingui/macro": "^5.6.0",
|
"@lingui/macro": "^5.6.0",
|
||||||
"@lingui/react": "^5.6.0",
|
"@lingui/react": "^5.6.0",
|
||||||
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
|
"@radix-ui/react-hover-card": "^1.1.15",
|
||||||
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
|
"@radix-ui/react-menubar": "^1.1.16",
|
||||||
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||||
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
|
"@radix-ui/react-radio-group": "^1.3.8",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
|
"@radix-ui/react-slider": "^1.3.6",
|
||||||
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@radix-ui/react-toast": "^1.2.15",
|
||||||
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@scure/base": "^1.2.6",
|
"@scure/base": "^1.2.6",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^0.2.1",
|
||||||
"colord": "^2.9.3",
|
"colord": "^2.9.3",
|
||||||
"framer-motion": "^12.43.0",
|
"framer-motion": "^12.43.0",
|
||||||
"lucide-react": "^0.554.0",
|
"lucide-react": "^0.554.0",
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
"pdfjs-dist": "5.4.296",
|
"pdfjs-dist": "5.4.296",
|
||||||
"perfect-freehand": "^1.2.2",
|
"perfect-freehand": "^1.2.2",
|
||||||
"react": "^19.2.7",
|
"react": "^18",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^18",
|
||||||
"react-hook-form": "^7.66.1",
|
"react-hook-form": "^7.66.1",
|
||||||
"react-rnd": "^10.5.2",
|
"react-rnd": "^10.5.2",
|
||||||
"remeda": "^2.32.0",
|
"remeda": "^2.32.0",
|
||||||
|
|||||||
@@ -145,9 +145,7 @@ const CommandItem = React.forwardRef<
|
|||||||
<CommandPrimitive.Item
|
<CommandPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
// cmdk 1.x always renders data-disabled="true|false", so the variant must
|
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
// check the value (bare data-[disabled] matches attribute presence).
|
|
||||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50',
|
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -103,10 +103,15 @@ export const FieldContent = ({ field, documentMeta }: FieldIconProps) => {
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-y-2 py-0.5">
|
<div className="flex flex-col gap-y-2 py-0.5">
|
||||||
<RadioGroup value={field.customText ?? ''} className="gap-y-1">
|
<RadioGroup className="gap-y-1">
|
||||||
{field.fieldMeta.values.map((item, index) => (
|
{field.fieldMeta.values.map((item, index) => (
|
||||||
<div key={index} className="flex items-center">
|
<div key={index} className="flex items-center">
|
||||||
<RadioGroupItem className="pointer-events-none h-3 w-3" value={item.value} id={`option-${index}`} />
|
<RadioGroupItem
|
||||||
|
className="pointer-events-none h-3 w-3"
|
||||||
|
value={item.value}
|
||||||
|
id={`option-${index}`}
|
||||||
|
checked={item.value === field.customText}
|
||||||
|
/>
|
||||||
{item.value && (
|
{item.value && (
|
||||||
<Label htmlFor={`option-${index}`} className="ml-1.5 font-normal text-foreground text-xs">
|
<Label htmlFor={`option-${index}`} className="ml-1.5 font-normal text-foreground text-xs">
|
||||||
{item.value}
|
{item.value}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { RecipientRole } from '@prisma/client';
|
import type { RecipientRole } from '@prisma/client';
|
||||||
import { BadgeCheck, Copy, Eye, PencilLine, User } from 'lucide-react';
|
import { BadgeCheck, Copy, Eye, PencilLine, User } from 'lucide-react';
|
||||||
import type { JSX } from 'react';
|
|
||||||
|
|
||||||
export const ROLE_ICONS: Record<RecipientRole, JSX.Element> = {
|
export const ROLE_ICONS: Record<RecipientRole, JSX.Element> = {
|
||||||
SIGNER: <PencilLine className="h-4 w-4" />,
|
SIGNER: <PencilLine className="h-4 w-4" />,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { getSvgPathFromStroke } from './helper';
|
|||||||
import { Point } from './point';
|
import { Point } from './point';
|
||||||
import { SignaturePadColorPicker } from './signature-pad-color-picker';
|
import { SignaturePadColorPicker } from './signature-pad-color-picker';
|
||||||
|
|
||||||
const checkSignatureValidity = (element: RefObject<HTMLCanvasElement | null>) => {
|
const checkSignatureValidity = (element: RefObject<HTMLCanvasElement>) => {
|
||||||
if (!element.current) {
|
if (!element.current) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import { generateId } from './utils/generate-id';
|
||||||
|
|
||||||
|
const JUSTIFICATIONS_DIR = join(process.cwd(), '.agents', 'justifications');
|
||||||
|
|
||||||
|
const main = () => {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length === 0) {
|
||||||
|
console.error('Usage: npx tsx scripts/create-justification.ts "file-slug" [content]');
|
||||||
|
console.error(' or: npx tsx scripts/create-justification.ts "file-slug" << HEREDOC');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const slug = args[0];
|
||||||
|
let content = '';
|
||||||
|
|
||||||
|
// Check if content is provided as second argument
|
||||||
|
if (args.length > 1) {
|
||||||
|
content = args.slice(1).join(' ');
|
||||||
|
} else {
|
||||||
|
// Read from stdin (heredoc)
|
||||||
|
try {
|
||||||
|
const stdin = readFileSync(0, 'utf-8');
|
||||||
|
content = stdin.trim();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading from stdin:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
console.error('Error: No content provided');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique ID
|
||||||
|
const id = generateId();
|
||||||
|
const filename = `${id}-${slug}.md`;
|
||||||
|
const filepath = join(JUSTIFICATIONS_DIR, filename);
|
||||||
|
|
||||||
|
// Format title from slug (kebab-case to Title Case)
|
||||||
|
const title = slug
|
||||||
|
.split('-')
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
// Get current date in ISO format
|
||||||
|
const date = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Create frontmatter
|
||||||
|
const frontmatter = `---
|
||||||
|
date: ${date}
|
||||||
|
title: ${title}
|
||||||
|
---
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
mkdirSync(JUSTIFICATIONS_DIR, { recursive: true });
|
||||||
|
|
||||||
|
// Write file with frontmatter
|
||||||
|
writeFileSync(filepath, frontmatter + content, 'utf-8');
|
||||||
|
|
||||||
|
console.log(`Created justification: ${filepath}`);
|
||||||
|
console.log(`ID: ${id}`);
|
||||||
|
console.log(`Filename: ${filename}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import { generateId } from './utils/generate-id';
|
||||||
|
|
||||||
|
const PLANS_DIR = join(process.cwd(), '.agents', 'plans');
|
||||||
|
|
||||||
|
const main = () => {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length === 0) {
|
||||||
|
console.error('Usage: npx tsx scripts/create-plan.ts "file-slug" [content]');
|
||||||
|
console.error(' or: npx tsx scripts/create-plan.ts "file-slug" << HEREDOC');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const slug = args[0];
|
||||||
|
let content = '';
|
||||||
|
|
||||||
|
// Check if content is provided as second argument
|
||||||
|
if (args.length > 1) {
|
||||||
|
content = args.slice(1).join(' ');
|
||||||
|
} else {
|
||||||
|
// Read from stdin (heredoc)
|
||||||
|
try {
|
||||||
|
const stdin = readFileSync(0, 'utf-8');
|
||||||
|
content = stdin.trim();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading from stdin:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
console.error('Error: No content provided');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique ID
|
||||||
|
const id = generateId();
|
||||||
|
const filename = `${id}-${slug}.md`;
|
||||||
|
const filepath = join(PLANS_DIR, filename);
|
||||||
|
|
||||||
|
// Format title from slug (kebab-case to Title Case)
|
||||||
|
const title = slug
|
||||||
|
.split('-')
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
// Get current date in ISO format
|
||||||
|
const date = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Create frontmatter
|
||||||
|
const frontmatter = `---
|
||||||
|
date: ${date}
|
||||||
|
title: ${title}
|
||||||
|
---
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
mkdirSync(PLANS_DIR, { recursive: true });
|
||||||
|
|
||||||
|
// Write file with frontmatter
|
||||||
|
writeFileSync(filepath, frontmatter + content, 'utf-8');
|
||||||
|
|
||||||
|
console.log(`Created plan: ${filepath}`);
|
||||||
|
console.log(`ID: ${id}`);
|
||||||
|
console.log(`Filename: ${filename}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import { generateId } from './utils/generate-id';
|
||||||
|
|
||||||
|
const SCRATCHES_DIR = join(process.cwd(), '.agents', 'scratches');
|
||||||
|
|
||||||
|
const main = () => {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length === 0) {
|
||||||
|
console.error('Usage: npx tsx scripts/create-scratch.ts "file-slug" [content]');
|
||||||
|
console.error(' or: npx tsx scripts/create-scratch.ts "file-slug" << HEREDOC');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const slug = args[0];
|
||||||
|
let content = '';
|
||||||
|
|
||||||
|
// Check if content is provided as second argument
|
||||||
|
if (args.length > 1) {
|
||||||
|
content = args.slice(1).join(' ');
|
||||||
|
} else {
|
||||||
|
// Read from stdin (heredoc)
|
||||||
|
try {
|
||||||
|
const stdin = readFileSync(0, 'utf-8');
|
||||||
|
content = stdin.trim();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading from stdin:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
console.error('Error: No content provided');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique ID
|
||||||
|
const id = generateId();
|
||||||
|
const filename = `${id}-${slug}.md`;
|
||||||
|
const filepath = join(SCRATCHES_DIR, filename);
|
||||||
|
|
||||||
|
// Format title from slug (kebab-case to Title Case)
|
||||||
|
const title = slug
|
||||||
|
.split('-')
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
// Get current date in ISO format
|
||||||
|
const date = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Create frontmatter
|
||||||
|
const frontmatter = `---
|
||||||
|
date: ${date}
|
||||||
|
title: ${title}
|
||||||
|
---
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
mkdirSync(SCRATCHES_DIR, { recursive: true });
|
||||||
|
|
||||||
|
// Write file with frontmatter
|
||||||
|
writeFileSync(filepath, frontmatter + content, 'utf-8');
|
||||||
|
|
||||||
|
console.log(`Created scratch: ${filepath}`);
|
||||||
|
console.log(`ID: ${id}`);
|
||||||
|
console.log(`Filename: ${filename}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* Generates a unique identifier using three simple words.
|
||||||
|
* Falls back to unix timestamp if word generation fails.
|
||||||
|
*/
|
||||||
|
export const generateId = (): string => {
|
||||||
|
const adjectives = [
|
||||||
|
'happy',
|
||||||
|
'bright',
|
||||||
|
'swift',
|
||||||
|
'calm',
|
||||||
|
'bold',
|
||||||
|
'clever',
|
||||||
|
'gentle',
|
||||||
|
'quick',
|
||||||
|
'sharp',
|
||||||
|
'warm',
|
||||||
|
'cool',
|
||||||
|
'fresh',
|
||||||
|
'solid',
|
||||||
|
'clear',
|
||||||
|
'sweet',
|
||||||
|
'wild',
|
||||||
|
'quiet',
|
||||||
|
'loud',
|
||||||
|
'smooth',
|
||||||
|
];
|
||||||
|
|
||||||
|
const nouns = [
|
||||||
|
'moon',
|
||||||
|
'star',
|
||||||
|
'ocean',
|
||||||
|
'river',
|
||||||
|
'forest',
|
||||||
|
'mountain',
|
||||||
|
'cloud',
|
||||||
|
'wave',
|
||||||
|
'stone',
|
||||||
|
'flower',
|
||||||
|
'bird',
|
||||||
|
'wind',
|
||||||
|
'light',
|
||||||
|
'shadow',
|
||||||
|
'fire',
|
||||||
|
'earth',
|
||||||
|
'sky',
|
||||||
|
'tree',
|
||||||
|
'leaf',
|
||||||
|
'rock',
|
||||||
|
];
|
||||||
|
|
||||||
|
const colors = [
|
||||||
|
'blue',
|
||||||
|
'red',
|
||||||
|
'green',
|
||||||
|
'yellow',
|
||||||
|
'purple',
|
||||||
|
'orange',
|
||||||
|
'pink',
|
||||||
|
'cyan',
|
||||||
|
'amber',
|
||||||
|
'emerald',
|
||||||
|
'violet',
|
||||||
|
'indigo',
|
||||||
|
'coral',
|
||||||
|
'teal',
|
||||||
|
'gold',
|
||||||
|
'silver',
|
||||||
|
'copper',
|
||||||
|
'bronze',
|
||||||
|
'ivory',
|
||||||
|
'jade',
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)];
|
||||||
|
const randomColor = colors[Math.floor(Math.random() * colors.length)];
|
||||||
|
const randomNoun = nouns[Math.floor(Math.random() * nouns.length)];
|
||||||
|
|
||||||
|
return `${randomAdjective}-${randomColor}-${randomNoun}`;
|
||||||
|
} catch {
|
||||||
|
// Fallback to unix timestamp if something goes wrong
|
||||||
|
return Date.now().toString();
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user