Compare commits

..

45 Commits

Author SHA1 Message Date
Jenil Savani
ad01e5af94 fix: adjust desktop nav search button width and spacing (#1699) 2025-03-13 10:35:24 +11:00
Ephraim Duncan
8890c5bee6 fix: signing field disabled when pointer is out of canvas (#1652) 2025-03-12 16:43:52 +11:00
Jenil Savani
6f39e89d30 fix: improve layout and truncate document information in logs page (#1656) 2025-03-12 16:29:48 +11:00
Tom
db90e1a34a chore: update French translations (#1679) 2025-03-12 16:16:44 +11:00
Mythie
e95e13021d v1.9.1-rc.9 2025-03-08 10:28:53 +11:00
Catalin Pit
5dfb17c216 fix: optional fields in embeds (#1691)
Fix optional fields blocking the signature process in embeds
2025-03-08 10:21:29 +11:00
Catalin Pit
27b81bc807 docs: prefill fields (#1688) 2025-03-07 19:25:28 +11:00
eddielu
6ce0be67ab docs: Update documentation to match reality. colorPrimary, colorBackground,… (#1666)
Update documentation to match reality. colorPrimary, colorBackground,
and borderRadius do not exist according to the schema:
280251cfdd/packages/react/src/css-vars.ts
2025-03-07 09:11:32 +11:00
Mythie
123a709836 v1.9.1-rc.8 2025-03-06 20:30:15 +11:00
Ephraim Duncan
ae6cc24317 feat: hide signature ui when theres no signature field (#1676) 2025-03-06 12:34:11 +11:00
Catalin Pit
a41ac632d0 feat: allow fields prefill when generating a document from a template (#1615)
This change allows API users to pre-fill fields with values by
passing the data in the request body. Example body for V2 API endpoint
`/api/v2-beta/template/use`:

```json
{
    "templateId": 1,
    "recipients": [
        {
            "id": 1,
            "email": "signer1@mail.com",
            "name": "Signer 1"
        },
        {
            "id": 2,
            "email": "signer2@mail.com",
            "name": "Signer 2"
        }
    ],
    "prefillValues": [
        {
            "id": 14,
            "fieldMeta": {
                "type": "text",
                "label": "my label",
                "placeholder": "text placeholder test",
                "text": "auto-sign value",
                "characterLimit": 25,
                "textAlign": "right",
                "fontSize": 94,
                "required": true
            }
        },
        {
            "id": 15,
            "fieldMeta": {
                "type": "radio",
                "label": "radio label",
                "placeholder": "new radio placeholder",
                "required": false,
                "readOnly": true,
                "values": [
                    {
                        "id": 2,
                        "checked": true,
                        "value": "radio val 1"
                    },
                    {
                        "id": 3,
                        "checked": false,
                        "value": "radio val 2"
                    }
                ]
            }
        },
        {
            "id": 16,
            "fieldMeta": {
                "type": "dropdown",
                "label": "dropdown label",
                "placeholder": "DD placeholder",
                "required": false,
                "readOnly": false,
                "values": [
                    {
                        "value": "option 1"
                    },
                    {
                        "value": "option 2"
                    },
                    {
                        "value": "option 3"
                    }
                ],
                "defaultValue": "option 2"
            }
        }
    ],
    "distributeDocument": false,
    "customDocumentDataId": ""
}
```
2025-03-06 09:44:09 +11:00
Catalin Pit
1789eff564 chore: add label for checkbox and radio fields (#1607) 2025-02-28 21:09:38 +11:00
Mythie
235d846d2b v1.9.1-rc.7 2025-02-28 10:11:36 +11:00
Mythie
ca3d65ad10 fix: remove auto-expand in embeddding 2025-02-28 10:11:08 +11:00
Mythie
617e3a46e0 v1.9.1-rc.6 2025-02-28 09:10:16 +11:00
David Nguyen
255c33cdab fix: stripe price fetch (#1677)
Currently Stripe prices search is omitting a price for an unknown
reason.

Changed our fetch logic to use `list` instead of `search` allows us to
work around the issue.

It's unknown on the performance impact of using `list` vs `search`
2025-02-28 09:04:25 +11:00
Mythie
1560218d4a v1.9.1-rc.5 2025-02-27 11:44:19 +11:00
Mythie
5c7768c253 fix: improve embed mobile experience 2025-02-27 11:43:42 +11:00
David Nimon
7bb93e4233 docs: add documentation for embedding via web components (#1670)
The web components version of embedding was missing documentation. I've
added it here. Let me know if there's anything that should be
changed/updated.
2025-02-26 16:04:05 +11:00
Mythie
42e39f7ef1 v1.9.1-rc.4 2025-02-26 15:28:51 +11:00
Mythie
838e399c73 fix: handle empty field meta for checkboxes 2025-02-26 15:27:44 +11:00
Catalin Pit
b6a891acc8 docs: add the v2 api staging base url (#1671) 2025-02-25 14:02:12 +02:00
Samuel Huber
84b4d58856 chore: update readme (#1664)
Resolve external link issue
2025-02-25 16:59:20 +11:00
Mythie
c51c32fdc6 feat: search by externalId 2025-02-25 09:44:40 +11:00
Mythie
59c1e55233 v1.9.1-rc.3 2025-02-25 08:03:13 +11:00
Mythie
2fbaf56c06 fix: early adopters can use platform features 2025-02-25 07:54:28 +11:00
David Nguyen
70320cd24b chore: update API documentation 2025-02-25 02:35:11 +11:00
Mythie
00b46561c2 v1.9.1-rc.2 2025-02-20 11:35:03 +11:00
Lucas Smith
11bc93a9a4 feat: allow document rejection in embeds (#1662)
Bing bang
2025-02-20 11:34:19 +11:00
David Nguyen
11528090a5 fix: prepare auth migration (#1648)
Add schema session migration in preparation for auth migration.
2025-02-18 15:17:47 +11:00
Ephraim Duncan
3c4863f285 chore: add asssitant role to the docs (#1638) 2025-02-17 15:42:37 +11:00
Ephraim Duncan
2ff330f9d4 chore: update local seed data (#1622)
## Description

Add multiple example documents, pending documents, and templates for
both admin and example users

## Changes Made
- Added seeding of multiple example documents and templates for both
example and admin users

## Checklist

- [x] I have tested these changes locally and they work as expected.
- [ ] I have added/updated tests that prove the effectiveness of these
changes.
- [ ] I have updated the documentation to reflect these changes, if
applicable.
- [x] I have followed the project's coding style guidelines.
- [x] I have addressed the code review feedback from the previous
submission, if applicable.
2025-02-10 22:55:12 +11:00
Mythie
ce1c93b2a6 v1.9.1-rc.1 2025-02-05 21:03:15 +11:00
Catalin Pit
82337e4e3a fix: typed signature not working (#1635)
The `typedSignatureEnabled` prop was removed from the `SignatureField`
component, which broke the typed signature meaning that nobody could
sign documents by typing their signature.
2025-02-05 21:02:21 +11:00
Mythie
7d9a3f9776 fix: assistant mode breaks for number fields 2025-02-04 07:59:41 +11:00
Mythie
cbad065dac v1.9.1-rc.0 2025-02-03 10:13:16 +11:00
Mythie
25a3861c91 fix: add css targets for embeds 2025-02-03 09:58:40 +11:00
Mythie
b9ae277041 v1.9.0 2025-02-03 09:33:08 +11:00
Mythie
7fad826d06 v1.9.0-rc.12 2025-02-01 15:53:18 +11:00
Lucas Smith
eb8ba2036a chore: add translations (#1619)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-02-01 15:52:21 +11:00
Catalin Pit
339759166c fix: temp field label/text truncation (#1565)
TEMP: Fix the truncation of the field label/text.
2025-02-01 14:35:19 +11:00
Ephraim Duncan
637e06f9c0 fix: unable to check on the checkbox field (#1593)
This change prevents race conditions between state updates and API
operations by updating local state immediately before making async
calls.
2025-02-01 14:34:42 +11:00
Ephraim Duncan
332e0657e0 feat: assistant role (#1588)
## Description

Introduces the ability for users with the **Assistant** role to prefill
fields on behalf of other signers. Assistants can fill in various field
types such as text, checkboxes, dates, and more, streamlining the
document preparation process before it reaches the final signers.

https://github.com/user-attachments/assets/c1321578-47ec-405b-a70a-7d9578385895
2025-02-01 14:31:18 +11:00
Timur Ercan
4017b250fb chore: api v2 docs (#1620)
chore update docs for api v2 announce
2025-01-31 09:11:47 +01:00
Mythie
41373a7c6f fix: improve move to team display logic 2025-01-31 11:33:08 +11:00
120 changed files with 4322 additions and 1440 deletions

View File

@@ -1,4 +1,4 @@
> 🚨 We are live on Product Hunt 🎉 Check out our latest launch: <a href="documen.so/sign-everywhere">The Platform Plan</a>!
> 🚨 We are live on Product Hunt 🎉 Check out our latest launch: <a href="https://documen.so/sign-everywhere">The Platform Plan</a>!
<a href="https://www.producthunt.com/posts/documenso-platform-plan?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-documenso&#0045;platform&#0045;plan" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=670576&theme=light" alt="Documenso&#0032;Platform&#0032;Plan - Whitelabeled&#0032;signing&#0032;flows&#0032;in&#0032;your&#0032;product | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>

View File

@@ -14,4 +14,4 @@
"public-api": "Public API",
"embedding": "Embedding",
"webhooks": "Webhooks"
}
}

View File

@@ -6,5 +6,6 @@
"solid": "Solid Integration",
"preact": "Preact Integration",
"angular": "Angular Integration",
"css-variables": "CSS Variables"
"css-variables": "CSS Variables",
"web-components": "Web Components"
}

View File

@@ -111,6 +111,83 @@ The colors will be automatically converted to the appropriate format internally.
4. **Consistent Radius**: Use a consistent border radius value that matches your application's design system.
## CSS Class Targets
In addition to CSS variables, specific components in the embedded experience can be targeted using CSS classes for more granular styling:
### Component Classes
| Class Name | Description |
| --------------------------------- | ----------------------------------------------------------------------- |
| `.embed--Root` | Main container for the embedded signing experience |
| `.embed--DocumentContainer` | Container for the document and signing widget |
| `.embed--DocumentViewer` | Container for the document viewer |
| `.embed--DocumentWidget` | The signing widget container |
| `.embed--DocumentWidgetContainer` | Outer container for the signing widget, handles positioning |
| `.embed--DocumentWidgetHeader` | Header section of the signing widget |
| `.embed--DocumentWidgetContent` | Main content area of the signing widget |
| `.embed--DocumentWidgetForm` | Form section within the signing widget |
| `.embed--DocumentWidgetFooter` | Footer section of the signing widget |
| `.embed--WaitingForTurn` | Container for the waiting screen when it's not the user's turn to sign |
| `.embed--DocumentCompleted` | Container for the completion screen after signing |
| `.field--FieldRootContainer` | Base container for document fields (signatures, text, checkboxes, etc.) |
Field components also expose several data attributes that can be used for styling different states:
| Data Attribute | Values | Description |
| ------------------- | ---------------------------------------------- | ------------------------------------ |
| `[data-field-type]` | `SIGNATURE`, `TEXT`, `CHECKBOX`, `RADIO`, etc. | The type of field |
| `[data-inserted]` | `true`, `false` | Whether the field has been filled |
| `[data-validate]` | `true`, `false` | Whether the field is being validated |
### Field Styling Example
```css
/* Style all field containers */
.field--FieldRootContainer {
transition: all 200ms ease;
}
/* Style specific field types */
.field--FieldRootContainer[data-field-type='SIGNATURE'] {
background-color: rgba(0, 0, 0, 0.02);
}
/* Style inserted fields */
.field--FieldRootContainer[data-inserted='true'] {
background-color: var(--primary);
opacity: 0.2;
}
/* Style fields being validated */
.field--FieldRootContainer[data-validate='true'] {
border-color: orange;
}
```
### Example Usage
```css
/* Custom styles for the document widget */
.embed--DocumentWidget {
background-color: #ffffff;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
/* Custom styles for the waiting screen */
.embed--WaitingForTurn {
background-color: #f9fafb;
padding: 2rem;
}
/* Responsive adjustments for the document container */
@media (min-width: 768px) {
.embed--DocumentContainer {
gap: 2rem;
}
}
```
## Related
- [React Integration](/developers/embedding/react)

View File

@@ -52,9 +52,9 @@ Platform customers have access to advanced styling options to customize the embe
<EmbedDirectTemplate
token={token}
cssVars={{
colorPrimary: '#0000FF',
colorBackground: '#F5F5F5',
borderRadius: '8px',
primary: '#0000FF',
background: '#F5F5F5',
radius: '8px',
}}
/>
```
@@ -73,14 +73,15 @@ These customization options are available for both Direct Templates and Signing
We support embedding across a range of popular JavaScript frameworks, including:
| Framework | Package |
| --------- | ---------------------------------------------------------------------------------- |
| React | [@documenso/embed-react](https://www.npmjs.com/package/@documenso/embed-react) |
| Preact | [@documenso/embed-preact](https://www.npmjs.com/package/@documenso/embed-preact) |
| Vue | [@documenso/embed-vue](https://www.npmjs.com/package/@documenso/embed-vue) |
| Svelte | [@documenso/embed-svelte](https://www.npmjs.com/package/@documenso/embed-svelte) |
| Solid | [@documenso/embed-solid](https://www.npmjs.com/package/@documenso/embed-solid) |
| Angular | [@documenso/embed-angular](https://www.npmjs.com/package/@documenso/embed-angular) |
| Framework | Package |
| --------- | ---------------------------------------------------------------------------------- |
| React | [@documenso/embed-react](https://www.npmjs.com/package/@documenso/embed-react) |
| Preact | [@documenso/embed-preact](https://www.npmjs.com/package/@documenso/embed-preact) |
| Vue | [@documenso/embed-vue](https://www.npmjs.com/package/@documenso/embed-vue) |
| Svelte | [@documenso/embed-svelte](https://www.npmjs.com/package/@documenso/embed-svelte) |
| Solid | [@documenso/embed-solid](https://www.npmjs.com/package/@documenso/embed-solid) |
| Angular | [@documenso/embed-angular](https://www.npmjs.com/package/@documenso/embed-angular) |
| Web Components | [@documenso/embed-webcomponent](https://www.npmjs.com/package/@documenso/embed-webcomponent) |
Additionally, we provide **web components** for more generalized use. However, please note that web components are still in their early stages and haven't been extensively tested.
@@ -166,6 +167,7 @@ Once you've obtained the appropriate tokens, you can integrate the signing exper
- [Svelte](/developers/embedding/svelte)
- [Solid](/developers/embedding/solid)
- [Angular](/developers/embedding/angular)
- [Web Components](/developers/embedding/web-components)
If you're using **web components**, the integration process is slightly different. Keep in mind that web components are currently less tested but can still provide flexibility for general use cases.
@@ -177,4 +179,5 @@ If you're using **web components**, the integration process is slightly differen
- [Solid Integration](/developers/embedding/solid)
- [Preact Integration](/developers/embedding/preact)
- [Angular Integration](/developers/embedding/angular)
- [Web Components](/developers/embedding/web-components)
- [CSS Variables](/developers/embedding/css-variables)

View File

@@ -95,9 +95,9 @@ const MyEmbeddingComponent = () => {
}
`;
const cssVars = {
colorPrimary: '#0000FF',
colorBackground: '#F5F5F5',
borderRadius: '8px',
primary: '#0000FF',
background: '#F5F5F5',
radius: '8px',
};
return (

View File

@@ -99,9 +99,9 @@ const MyEmbeddingComponent = () => {
`}
// CSS Variables
cssVars={{
colorPrimary: '#0000FF',
colorBackground: '#F5F5F5',
borderRadius: '8px',
primary: '#0000FF',
background: '#F5F5F5',
radius: '8px',
}}
// Dark Mode Control
darkModeDisabled={true}

View File

@@ -95,9 +95,9 @@ const MyEmbeddingComponent = () => {
}
`;
const cssVars = {
colorPrimary: '#0000FF',
colorBackground: '#F5F5F5',
borderRadius: '8px',
primary: '#0000FF',
background: '#F5F5F5',
radius: '8px',
};
return (

View File

@@ -97,9 +97,9 @@ Platform customers have access to advanced styling options:
}
`;
const cssVars = {
colorPrimary: '#0000FF',
colorBackground: '#F5F5F5',
borderRadius: '8px',
primary: '#0000FF',
background: '#F5F5F5',
radius: '8px',
};
</script>

View File

@@ -97,9 +97,9 @@ Platform customers have access to advanced styling options:
}
`;
const cssVars = {
colorPrimary: '#0000FF',
colorBackground: '#F5F5F5',
borderRadius: '8px',
primary: '#0000FF',
background: '#F5F5F5',
radius: '8px',
};
</script>

View File

@@ -0,0 +1,89 @@
---
title: Web Components Integration
description: Learn how to use our embedding SDK via Web Components on a framework-less web application.
---
# Web Components Integration
Our Web Components SDK provides a simple way to embed a signing experience within your framework-less web application. It supports both direct link templates and signing tokens.
## Installation
To install the SDK, run the following command:
```bash
npm install @documenso/embed-webcomponent
```
Then in your html file, add the following to add the script, replacing the path with the proper path to the web component script.
```html
<script src="YOUR_PATH_HERE"></script>
```
## Usage
To embed a signing experience, you'll need to provide the token for the document you want to embed. This can be done in a few different ways, depending on your use case.
### Direct Link Template
If you have a direct link template, you can simply provide the token for the template to the `documenso-embed-direct-template` tag.
```html
<documenso-embed-direct-template
token="YOUR_TOKEN_HERE"
</documenso-embed-direct-template>
```
#### Attributes
| Attribute | Type | Description |
| ------------------- | ------------------- | ------------------------------------------------------------------------------------------ |
| token | string | The token for the document you want to embed |
| host | string (optional) | The host to be used for the signing experience, relevant for self-hosters |
| name | string (optional) | The name the signer that will be used by default for signing |
| lockName | boolean (optional) | Whether or not the name field should be locked disallowing modifications |
| email | string (optional) | The email the signer that will be used by default for signing |
| lockEmail | boolean (optional) | Whether or not the email field should be locked disallowing modifications |
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |
| onFieldSigned | function (optional) | A callback function that will be called when a field is signed |
| onFieldUnsigned | function (optional) | A callback function that will be called when a field is unsigned |
### Signing Token
If you have a signing token, you can provide it to the `documenso-embed-sign-document` tag.
```html
<documenso-embed-sign-document
token="YOUR_TOKEN_HERE"
</documenso-embed-sign-document>
```
#### Attributes
| Attribute | Type | Description |
| ------------------- | ------------------- | ------------------------------------------------------------------------------------------ |
| token | string | The token for the document you want to embed |
| host | string (optional) | The host to be used for the signing experience, relevant for self-hosters |
| name | string (optional) | The name the signer that will be used by default for signing |
| lockName | boolean (optional) | Whether or not the name field should be locked disallowing modifications |
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |
### Creating via JavaScript
You can also create the tag element using javascript, for dynamic generation of either modes. For example, this would add the sign document embed to the DOM.
```javascript
document.getElementById('my-wrapper-here').innerHTML = '';
const tag = document.createElement('documenso-embed-sign-document');
tag.setAttribute('token', data.token);
tag.style.width = '100%';
tag.style.height = '100%';
document.getElementById('my-wrapper-here').appendChild(tag);
```

View File

@@ -3,6 +3,8 @@ title: Public API
description: Learn how to interact with your documents programmatically using the Documenso public API.
---
import { Callout, Steps } from 'nextra/components';
# Public API
Documenso provides a public REST API enabling you to interact with your documents programmatically. The API exposes various HTTP endpoints that allow you to perform operations such as:
@@ -13,10 +15,35 @@ Documenso provides a public REST API enabling you to interact with your document
The documentation walks you through creating API keys and using them to authenticate your API requests. You'll also learn about the available endpoints, request and response formats, and how to use the API.
## Swagger Documentation
## API V1 - Stable
The [Swagger documentation](https://app.documenso.com/api/v1/openapi) also provides information about the API endpoints, request parameters, response formats, and authentication methods.
Check out the [API V1 documentation](https://app.documenso.com/api/v1/openapi) for details about the API endpoints, request parameters, response formats, and authentication methods.
## API V2 - Beta
<Callout type="warning">API V2 is currently beta, and will be subject to breaking changes</Callout>
Check out the [API V2 documentation](https://documen.so/api-v2-docs) for details about the API endpoints, request parameters, response formats, and authentication methods.
Our new API V2 supports the following typed SDKs:
- [TypeScript](https://github.com/documenso/sdk-typescript)
- [Python](https://github.com/documenso/sdk-python)
- [Go](https://github.com/documenso/sdk-go)
<Callout type="info">
For the staging API, please use the following base URL:
`https://stg-app.documenso.dev/api/v2-beta/`
</Callout>
🚀 [V2 Announcement](https://documen.so/sdk-blog)
📖 [Documentation](https://documen.so/api-v2-docs)
💬 [Leave Feedback](https://documen.so/sdk-feedback)
🔔 [Breaking Changes](https://documen.so/sdk-breaking)
## Availability
The API is available to individual users and teams.
The API is available to individual users, teams and higher plans. [Fair Use](https://documen.so/fair) applies.

View File

@@ -532,3 +532,93 @@ Replace the `text` value with the corresponding field type:
- For the `SELECT` field it should be `select`. (check this before merge)
You must pass this property at all times, even if you don't need to set any other properties. If you don't, the endpoint will throw an error.
## Pre-fill Fields On Document Creation
The API allows you to pre-fill fields on document creation. This is useful when you want to create a document from an existing template and pre-fill the fields with specific values.
To pre-fill a field, you need to make a `POST` request to the `/api/v1/templates/{templateId}/generate-document` endpoint with the field information. Here's an example:
```json
{
"title": "my-document.pdf",
"recipients": [
{
"id": 3,
"name": "Example User",
"email": "example@documenso.com",
"signingOrder": 1,
"role": "SIGNER"
}
],
"prefillFields": [
{
"id": 21,
"type": "text",
"label": "my-label",
"placeholder": "my-placeholder",
"value": "my-value"
},
{
"id": 22,
"type": "number",
"label": "my-label",
"placeholder": "my-placeholder",
"value": "123"
},
{
"id": 23,
"type": "checkbox",
"label": "my-label",
"placeholder": "my-placeholder",
"value": ["option-1", "option-2"]
}
]
}
```
Check out the endpoint in the [API V1 documentation](https://app.documenso.com/api/v1/openapi#:~:text=/%7BtemplateId%7D/-,generate,-%2Ddocument).
### API V2
For API V2, you need to make a `POST` request to the `/api/v2-beta/template/use` endpoint with the field(s) information. Here's an example:
```json
{
"templateId": 111,
"recipients": [
{
"id": 3,
"name": "Example User",
"email": "example@documenso.com",
"signingOrder": 1,
"role": "SIGNER"
}
],
"prefillFields": [
{
"id": 21,
"type": "text",
"label": "my-label",
"placeholder": "my-placeholder",
"value": "my-value"
},
{
"id": 22,
"type": "number",
"label": "my-label",
"placeholder": "my-placeholder",
"value": "123"
},
{
"id": 23,
"type": "checkbox",
"label": "my-label",
"placeholder": "my-placeholder",
"value": ["option-1", "option-2"]
}
]
}
```
Check out the endpoint in the [API V2 documentation](https://openapi.documenso.com/reference#tag/template/POST/template/use).

View File

@@ -85,12 +85,13 @@ You can also set the recipient's role, which determines their actions and permis
Documenso has 4 roles for recipients with different permissions and actions.
| Role | Function | Action required | Signature |
| :------: | :-----------------------------------------------------------------------------: | :-------------: | :-------: |
| Signer | Needs to sign signatures fields assigned to them. | Yes | Yes |
| Approver | Needs to approve the document as a whole. Signature optional. | Yes | Optional |
| Viewer | Needs to confirm they viewed the document. | Yes | No |
| BCC | Receives a copy of the signed document after completion. No action is required. | No | No |
| Role | Function | Action required | Signature |
| :-------: | :-----------------------------------------------------------------------------: | :-------------: | :-------: |
| Signer | Needs to sign signatures fields assigned to them. | Yes | Yes |
| Approver | Needs to approve the document as a whole. Signature optional. | Yes | Optional |
| Viewer | Needs to confirm they viewed the document. | Yes | No |
| Assistant | Can help prepare the document by filling in fields on behalf of other signers. | Yes | No |
| CC | Receives a copy of the signed document after completion. No action is required. | No | No |
### Fields

View File

@@ -1,6 +1,6 @@
{
"name": "@documenso/web",
"version": "1.9.0-rc.11",
"version": "1.9.1-rc.9",
"private": true,
"license": "AGPL-3.0",
"scripts": {

View File

@@ -1,19 +0,0 @@
<svg width="58" height="58" viewBox="0 0 58 58" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.07 6.25832C23.333 6.92796 22.5176 7.71453 21.5857 8.63408C20.9957 9.09732 20.2682 9.35873 19.5117 9.37328L16.3896 9.43332L17.2992 8.52369C22.815 3.0079 25.5729 0.25 29 0.25C32.4271 0.25 35.185 3.00789 40.7008 8.52368L41.6087 9.43166L38.5937 9.37486C37.7437 9.35885 36.9292 9.03109 36.3051 8.45388L34.4972 6.78198C34.3255 6.6212 34.1581 6.46631 33.9946 6.31712L33.897 6.22687L33.8953 6.22687C33.2778 5.667 32.7153 5.18958 32.1851 4.78508C30.6538 3.6167 29.7624 3.35263 29 3.35263C28.2376 3.35263 27.3462 3.6167 25.8149 4.78508C25.2783 5.19451 24.7085 5.67864 24.0821 6.24737L24.0814 6.24737L24.07 6.25832Z" fill="black"/>
<path d="M51.6826 24.0051C51.5337 23.8419 51.3791 23.6748 51.2187 23.5035L49.5459 21.6946C48.9691 21.0709 48.6413 20.2571 48.6249 19.4077L48.5667 16.3896L49.4763 17.2992C54.9921 22.815 57.75 25.5729 57.75 29C57.75 32.4271 54.9921 35.185 49.4763 40.7008L48.5667 41.6104L48.6249 38.5923C48.6413 37.7429 48.9691 36.9291 49.5459 36.3054L51.2185 34.4968C51.379 34.3253 51.5337 34.1581 51.6827 33.9948L51.7731 33.897V33.8953C52.333 33.2778 52.8104 32.7153 53.2149 32.1851C54.3833 30.6538 54.6474 29.7624 54.6474 29C54.6474 28.2376 54.3833 27.3462 53.2149 25.8149C52.8104 25.2847 52.333 24.7222 51.7731 24.1047V24.103L51.6826 24.0051Z" fill="black"/>
<path d="M33.9601 51.7143C34.1446 51.5464 34.334 51.3711 34.5289 51.1883L36.3054 49.5457C36.9294 48.9687 37.7435 48.6411 38.5932 48.6249L41.6096 48.5675L40.7008 49.4763C35.185 54.9921 32.4271 57.75 29 57.75C25.5729 57.75 22.815 54.9921 17.2992 49.4763L16.3896 48.5667L19.4141 48.6248C20.2599 48.641 21.0705 48.9659 21.6934 49.5383L22.9131 50.6592C24.0267 51.726 24.9626 52.5647 25.8149 53.2149C27.3462 54.3833 28.2376 54.6474 29 54.6474C29.7624 54.6474 30.6538 54.3833 32.1851 53.2149C32.7217 52.8055 33.2915 52.3214 33.9179 51.7526H33.9187L33.9601 51.7143Z" fill="black"/>
<path d="M6.26202 33.9341C6.44675 34.1373 6.64036 34.3465 6.8432 34.5625L8.4547 36.3051C9.03166 36.929 9.35938 37.7431 9.37562 38.5927L9.43332 41.6104L8.52369 40.7008C3.0079 35.185 0.25 32.4271 0.25 29C0.25 25.5729 3.00789 22.815 8.52368 17.2992L9.43249 16.3904L9.37539 19.4067C9.3593 20.2567 9.03143 21.0711 8.45413 21.6952L6.79271 23.4913C6.62762 23.6675 6.46871 23.8392 6.3158 24.0069L6.22687 24.103L6.22687 24.1047C5.66699 24.7222 5.18958 25.2847 4.78508 25.8149C3.6167 27.3462 3.35263 28.2376 3.35263 29C3.35263 29.7624 3.6167 30.6538 4.78508 32.1851C5.1946 32.7219 5.67887 33.2918 6.24777 33.9184L6.24777 33.9187L6.26202 33.9341Z" fill="black"/>
<path d="M6.24777 24.0804L8.45413 21.6952C8.96576 21.1421 9.28147 20.4395 9.35788 19.6951C9.36697 18.4688 9.38705 17.3991 9.43129 16.4536L9.43374 16.3242L9.43774 16.3202C9.47846 15.5034 9.53816 14.7805 9.62565 14.1297C9.88231 12.2207 10.3259 11.4037 10.865 10.8646C11.4041 10.3255 12.2211 9.88195 14.1301 9.62529C14.7929 9.53618 15.5306 9.4759 16.3661 9.43513L16.3675 9.43374L16.4131 9.43287C17.3923 9.38614 18.5054 9.36574 19.7886 9.35686C20.5626 9.2798 21.2914 8.94412 21.8545 8.39979L24.0813 6.24742H22.7951C14.9946 6.24742 11.0944 6.24742 8.67108 8.67072C6.24777 11.094 6.24777 14.9943 6.24777 22.7948V24.0804Z" fill="black"/>
<path d="M6.24777 33.9187V35.2053C6.24777 43.0058 6.24777 46.9061 8.67108 49.3294C11.0944 51.7527 14.9946 51.7527 22.7951 51.7527H35.2057C43.0062 51.7527 46.9064 51.7527 49.3297 49.3294C51.753 46.9061 51.753 43.0058 51.753 35.2053V33.9187L49.5459 36.3054C49.0356 36.8571 48.7203 37.5577 48.643 38.3C48.6337 39.5529 48.613 40.6424 48.5668 41.603L48.5663 41.6325L48.5654 41.6334C48.5246 42.4693 48.4643 43.2073 48.3752 43.8704C48.1185 45.7794 47.6749 46.5964 47.1358 47.1355C46.5967 47.6746 45.7797 48.1181 43.8707 48.3748C43.2197 48.4623 42.4965 48.522 41.6793 48.5628L41.6758 48.5663L41.5626 48.5684C40.6127 48.6132 39.5373 48.6334 38.3032 48.6426C37.5597 48.7193 36.858 49.0347 36.3054 49.5457L33.9187 51.7526L24.103 51.7526L21.6934 49.5383C21.1424 49.032 20.4445 48.7193 19.7052 48.6426C18.4558 48.6334 17.3688 48.6129 16.4101 48.5671L16.3675 48.5663L16.3662 48.565C15.5307 48.5242 14.7929 48.4639 14.1301 48.3748C12.2211 48.1181 11.4041 47.6746 10.865 47.1355C10.3259 46.5964 9.88231 45.7794 9.62565 43.8704C9.53653 43.2075 9.47625 42.4698 9.43548 41.6342L9.43374 41.6325L9.43265 41.5753C9.38742 40.6221 9.36703 39.5422 9.35786 38.3022C9.281 37.559 8.96554 36.8575 8.4547 36.3051L6.24777 33.9187Z" fill="black"/>
<path d="M48.643 19.7C48.7203 20.4423 49.0356 21.1428 49.5459 21.6946L51.753 24.0813V22.7948C51.753 14.9943 51.753 11.094 49.3297 8.67072C46.9064 6.24742 43.0062 6.24742 35.2057 6.24742H33.9192L36.3051 8.45388C36.8586 8.96577 37.5618 9.28147 38.3067 9.35754C39.5257 9.36658 40.5898 9.38648 41.531 9.4302L41.7192 9.43374L41.725 9.43963C42.5235 9.4803 43.2319 9.5394 43.8707 9.62529C45.7797 9.88195 46.5967 10.3255 47.1358 10.8646C47.6749 11.4037 48.1185 12.2207 48.3752 14.1297C48.4643 14.7928 48.5246 15.5307 48.5654 16.3666L48.5663 16.3675L48.5668 16.3971C48.613 17.3577 48.6337 18.4471 48.643 19.7Z" fill="black"/>
<path d="M26.6453 15.2538L24.5526 17.0299C24.1792 17.3469 23.7112 17.5312 23.2219 17.554L19.7195 17.7172L21.5458 15.8909C25.071 12.3656 26.8336 10.603 29.0239 10.603C31.2142 10.603 32.9768 12.3656 36.502 15.8909L38.3172 17.706L34.7657 17.5521C34.2678 17.5305 33.7917 17.3417 33.4143 17.0162L31.8345 15.6533C31.2799 15.1314 30.8096 14.7189 30.3805 14.3915C29.5014 13.7207 29.168 13.7055 29.0239 13.7055C28.8799 13.7055 28.5465 13.7207 27.6674 14.3915C27.6533 14.4022 27.6393 14.413 27.6252 14.4239L27.6232 14.4238L27.6024 14.4414C27.3079 14.6698 26.9934 14.9381 26.6453 15.2538Z" fill="black"/>
<path d="M43.4935 27.471C42.8957 26.7152 42.0376 25.8229 40.7954 24.5736C40.5923 24.2491 40.4753 23.8751 40.4596 23.4874L40.306 19.6948L42.1106 21.4994C45.6358 25.0247 47.3985 26.7873 47.3985 28.9776C47.3985 31.1678 45.6358 32.9304 42.1106 36.4557L40.2963 38.27L40.4711 34.6452C40.4954 34.1415 40.6908 33.6612 41.025 33.2835L42.352 31.784C42.7709 31.3388 43.1192 30.9479 43.4095 30.589L43.573 30.4043V30.3824C43.5854 30.3662 43.5978 30.3502 43.61 30.3341C44.2808 29.455 44.296 29.1216 44.296 28.9776C44.296 28.8335 44.2808 28.5001 43.61 27.621C43.5978 27.6049 43.5854 27.5889 43.573 27.5727V27.5545L43.4935 27.471Z" fill="black"/>
<path d="M14.4826 27.5691L16.902 24.9381C17.2447 24.5655 17.4494 24.0868 17.4821 23.5816L17.616 21.5163C17.6363 20.8624 17.6699 20.31 17.7254 19.8285L17.7349 19.682L17.7445 19.6725C17.7466 19.6559 17.7488 19.6394 17.751 19.6229C17.8983 18.527 18.1233 18.2805 18.2251 18.1786C18.327 18.0767 18.5736 17.8518 19.6695 17.7044C20.0213 17.6571 20.4118 17.6233 20.8544 17.5991L23.8259 17.3059C24.3027 17.2589 24.7513 17.0586 25.1046 16.7352L27.6159 14.4361H25.0583C20.0729 14.4361 17.5802 14.4361 16.0314 15.9848C14.712 17.3043 14.5166 19.3088 14.4877 22.9561C14.4826 23.59 14.4826 24.2735 14.4826 25.0117L14.4826 27.5627V27.5691Z" fill="black"/>
<path d="M14.4826 30.386V30.3952L14.4826 32.9434C14.4826 33.6816 14.4826 34.3651 14.4877 34.999C14.5166 38.6463 14.7119 40.6509 16.0314 41.9703C17.3509 43.2898 19.3554 43.4851 23.0028 43.5141C23.6366 43.5191 24.3201 43.5191 25.0583 43.5191H27.6059H30.4384H32.99C33.728 43.5191 34.4114 43.5191 35.0451 43.5141C38.6927 43.4852 40.6974 43.2898 42.0169 41.9703C43.5657 40.4216 43.5657 37.9289 43.5657 32.9434V30.3783L40.9873 33.1741C40.7295 33.4537 40.5498 33.7928 40.462 34.1575C40.457 35.9697 40.4324 37.2297 40.3142 38.1998L40.313 38.2515L40.3071 38.2574C40.3039 38.2825 40.3006 38.3075 40.2973 38.3322C40.15 39.4282 39.925 39.6747 39.8231 39.7766C39.7213 39.8784 39.4747 40.1034 38.3788 40.2507C38.3541 40.2541 38.3291 40.2573 38.304 40.2605L38.2979 40.2666L38.2111 40.2719C37.6594 40.3374 37.0141 40.3733 36.2282 40.3929L34.4602 40.5008C33.9569 40.5316 33.4791 40.7331 33.1058 41.072L30.4107 43.5191H27.5938L24.8997 41.0003C24.5451 40.6688 24.0925 40.4638 23.612 40.4147C23.4039 40.4139 23.2033 40.4127 23.0098 40.4112C21.5576 40.3995 20.5048 40.363 19.6695 40.2507C18.5736 40.1034 18.327 39.8784 18.2251 39.7766C18.1233 39.6747 17.8983 39.4282 17.751 38.3322C17.7488 38.3158 17.7466 38.2993 17.7445 38.2828L17.7349 38.2732L17.734 38.2524L17.7304 38.1697C17.6321 37.3441 17.6002 36.3092 17.5899 34.9063L17.5744 34.5469C17.5703 34.4505 17.5598 34.355 17.5434 34.2608C17.4714 33.8486 17.2836 33.463 16.9993 33.1507L14.4826 30.386Z" fill="black"/>
<path d="M40.4361 21.6459L40.5814 23.6165C40.6181 24.1136 40.8212 24.5837 41.158 24.9511L43.5657 27.5768V25.0117C43.5657 20.0263 43.5657 17.5336 42.0169 15.9848C40.4681 14.4361 37.9754 14.4361 32.99 14.4361H30.4104L32.9941 16.7897C33.3562 17.1196 33.8173 17.3201 34.3055 17.3601L37.3018 17.6052C37.5881 17.6223 37.8522 17.6436 38.098 17.6704L38.3195 17.6885L38.3288 17.6978C38.3455 17.7 38.3622 17.7022 38.3788 17.7044C39.4747 17.8518 39.7213 18.0767 39.8231 18.1786C39.925 18.2805 40.15 18.527 40.2973 19.6229C40.3724 20.1817 40.4136 20.8379 40.4361 21.6459Z" fill="black"/>
<path d="M17.7437 38.2621L17.734 38.2524L17.7349 38.2732L17.7445 38.2828L17.7437 38.2621Z" fill="black"/>
<path d="M19.7418 40.2602L23.0098 40.4112C21.5992 40.3998 20.5654 40.3651 19.7418 40.2602Z" fill="black"/>
<path d="M17.6049 34.6945C17.5987 34.5474 17.578 34.4022 17.5434 34.2608C17.5598 34.355 17.5703 34.4505 17.5744 34.5469L17.5899 34.9063C17.6002 36.3092 17.6321 37.3441 17.7304 38.1697L17.734 38.2524L17.7437 38.2621L17.6049 34.6945Z" fill="black"/>
<path d="M10.6494 28.9776C10.6494 30.8436 11.9288 32.3993 14.4877 34.999C14.4826 34.3651 14.4826 33.6816 14.4826 32.9434L14.4826 30.3952L14.4772 30.389L14.4772 30.3854C14.464 30.3682 14.4508 30.3511 14.4379 30.3341C13.7671 29.455 13.7518 29.1216 13.7518 28.9776C13.7518 28.8335 13.7671 28.5001 14.4379 27.621C14.4526 27.6016 14.4675 27.5822 14.4826 27.5627L14.4826 25.0117C14.4826 24.2735 14.4826 23.59 14.4877 22.9561C11.9288 25.5558 10.6494 27.1115 10.6494 28.9776Z" fill="black"/>
<path d="M27.6674 43.5636C27.6549 43.5541 27.6425 43.5446 27.63 43.5349H27.6232L27.6059 43.5191H25.0583C24.3201 43.5191 23.6366 43.5191 23.0028 43.5141C25.6023 46.0727 27.1579 47.3521 29.0239 47.3521C30.8899 47.3521 32.4455 46.0727 35.0451 43.5141C34.4114 43.5191 33.728 43.5191 32.99 43.5191H30.4384C30.419 43.5341 30.3997 43.5489 30.3805 43.5636C29.5014 44.2344 29.168 44.2496 29.0239 44.2496C28.8799 44.2496 28.5465 44.2344 27.6674 43.5636Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -12,13 +12,14 @@ import {
MailOpenIcon,
PenIcon,
PlusIcon,
UserIcon,
} from 'lucide-react';
import { match } from 'ts-pattern';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { formatSigningLink } from '@documenso/lib/utils/recipients';
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import type { Document, Recipient } from '@documenso/prisma/client';
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
import { SignatureIcon } from '@documenso/ui/icons/signature';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
@@ -120,6 +121,12 @@ export const DocumentPageViewRecipients = ({
<Trans>Viewed</Trans>
</>
))
.with(RecipientRole.ASSISTANT, () => (
<>
<UserIcon className="mr-1 h-3 w-3" />
<Trans>Assisted</Trans>
</>
))
.exhaustive()}
</Badge>
)}

View File

@@ -119,7 +119,7 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
<Trans>Document</Trans>
</Link>
<div className="flex flex-col justify-between truncate sm:flex-row">
<div className="flex flex-col">
<div>
<h1
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
@@ -127,7 +127,8 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
>
{document.title}
</h1>
</div>
<div className="mt-1 flex flex-col justify-between sm:flex-row">
<div className="mt-2.5 flex items-center gap-x-6">
<DocumentStatusComponent
inheritColor
@@ -135,17 +136,16 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
className="text-muted-foreground"
/>
</div>
</div>
<div className="mt-4 flex w-full flex-row sm:mt-0 sm:w-auto sm:self-end">
<DownloadCertificateButton
className="mr-2"
documentId={document.id}
documentStatus={document.status}
teamId={team?.id}
/>
<div className="mt-4 flex w-full flex-row sm:mt-0 sm:w-auto sm:self-end">
<DownloadCertificateButton
className="mr-2"
documentId={document.id}
documentStatus={document.status}
teamId={team?.id}
/>
<DownloadAuditLogButton teamId={team?.id} documentId={document.id} />
<DownloadAuditLogButton teamId={team?.id} documentId={document.id} />
</div>
</div>
</div>
@@ -154,7 +154,7 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
{documentInformation.map((info, i) => (
<div className="text-foreground text-sm" key={i}>
<h3 className="font-semibold">{_(info.description)}</h3>
<p className="text-muted-foreground">{info.value}</p>
<p className="text-muted-foreground truncate">{info.value}</p>
</div>
))}

View File

@@ -162,7 +162,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
</DropdownMenuItem>
{/* We don't want to allow teams moving documents across at the moment. */}
{!team && (
{!team && !row.teamId && (
<DropdownMenuItem onClick={() => setMoveDialogOpen(true)}>
<MoveRight className="mr-2 h-4 w-4" />
<Trans>Move to Team</Trans>

View File

@@ -40,7 +40,7 @@ export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocum
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
const { data: teams, isPending: isLoadingTeams } = trpc.team.getTeams.useQuery();
const { mutateAsync: moveDocument, isPending } = trpc.document.moveDocumentToTeam.useMutation({
onSuccess: () => {

View File

@@ -81,7 +81,7 @@ export const DataTableActionDropdown = ({
<Trans>Direct link</Trans>
</DropdownMenuItem>
{!teamId && (
{!teamId && !row.teamId && (
<DropdownMenuItem onClick={() => setMoveDialogOpen(true)}>
<MoveRight className="mr-2 h-4 w-4" />
<Trans>Move to Team</Trans>

View File

@@ -42,7 +42,7 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
const { data: teams, isPending: isLoadingTeams } = trpc.team.getTeams.useQuery();
const { mutateAsync: moveTemplate, isPending } = trpc.template.moveTemplateToTeam.useMutation({
onSuccess: () => {
router.refresh();

View File

@@ -77,7 +77,11 @@ export const TemplateDirectLinkDialog = ({
);
const validDirectTemplateRecipients = useMemo(
() => template.recipients.filter((recipient) => recipient.role !== RecipientRole.CC),
() =>
template.recipients.filter(
(recipient) =>
recipient.role !== RecipientRole.CC && recipient.role !== RecipientRole.ASSISTANT,
),
[template.recipients],
);

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { redirect } from 'next/navigation';
import { msg } from '@lingui/macro';
@@ -5,10 +7,8 @@ import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
import { UAParser } from 'ua-parser-js';
import { renderSVG } from 'uqr';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { APP_I18N_OPTIONS, ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n';
import {
RECIPIENT_ROLES_DESCRIPTION,
@@ -73,8 +73,6 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
id: documentId,
}).catch(() => null);
const documentAccessToken = document?.documentAccessToken?.token;
if (!document) {
return redirect('/');
}
@@ -307,27 +305,17 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
</TableBody>
</Table>
</CardContent>
<div className="my-8 flex flex-row-reverse items-end justify-between px-8">
<div className="flex items-end justify-end gap-x-4">
<div
className="flex h-24 w-24 justify-center"
dangerouslySetInnerHTML={{
__html: renderSVG(`${WEBAPP_BASE_URL}/q/${documentAccessToken}`, {
ecc: 'Q',
}),
}}
/>
</div>
<div>
<p className="flex-shrink-0 text-sm print:text-xs">
{_(msg`Signing certificate provided by`)}:
</p>
<Logo className="mt-2 max-h-6 print:max-h-4" />
</div>
</div>
</Card>
<div className="my-8 flex-row-reverse">
<div className="flex items-end justify-end gap-x-4">
<p className="flex-shrink-0 text-sm font-medium print:text-xs">
{_(msg`Signing certificate provided by`)}:
</p>
<Logo className="max-h-6 print:max-h-4" />
</div>
</div>
</div>
);
}

View File

@@ -47,6 +47,7 @@ import { NameField } from '~/app/(signing)/sign/[token]/name-field';
import { NumberField } from '~/app/(signing)/sign/[token]/number-field';
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
import { RadioField } from '~/app/(signing)/sign/[token]/radio-field';
import { RecipientProvider } from '~/app/(signing)/sign/[token]/recipient-context';
import { SignDialog } from '~/app/(signing)/sign/[token]/sign-dialog';
import { SignatureField } from '~/app/(signing)/sign/[token]/signature-field';
import { TextField } from '~/app/(signing)/sign/[token]/text-field';
@@ -89,7 +90,7 @@ export const SignDirectTemplateForm = ({
const tempField: DirectTemplateLocalField = {
...field,
customText: value.value,
customText: value.value ?? '',
inserted: true,
signedValue: value,
};
@@ -100,8 +101,8 @@ export const SignDirectTemplateForm = ({
created: new Date(),
recipientId: 1,
fieldId: 1,
signatureImageAsBase64: value.value.startsWith('data:') ? value.value : null,
typedSignature: value.value.startsWith('data:') ? null : value.value,
signatureImageAsBase64: value.value?.startsWith('data:') ? value.value : null,
typedSignature: value.value && !value.value.startsWith('data:') ? value.value : null,
} satisfies Signature;
}
@@ -169,7 +170,7 @@ export const SignDirectTemplateForm = ({
};
return (
<>
<RecipientProvider recipient={directRecipient}>
<DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} />
<DocumentFlowFormContainerContent>
@@ -186,16 +187,15 @@ export const SignDirectTemplateForm = ({
<SignatureField
key={field.id}
field={field}
recipient={directRecipient}
onSignField={onSignField}
onUnsignField={onUnsignField}
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
/>
))
.with(FieldType.INITIALS, () => (
<InitialsField
key={field.id}
field={field}
recipient={directRecipient}
onSignField={onSignField}
onUnsignField={onUnsignField}
/>
@@ -204,7 +204,6 @@ export const SignDirectTemplateForm = ({
<NameField
key={field.id}
field={field}
recipient={directRecipient}
onSignField={onSignField}
onUnsignField={onUnsignField}
/>
@@ -213,7 +212,6 @@ export const SignDirectTemplateForm = ({
<DateField
key={field.id}
field={field}
recipient={directRecipient}
dateFormat={template.templateMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
timezone={template.templateMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE}
onSignField={onSignField}
@@ -224,7 +222,6 @@ export const SignDirectTemplateForm = ({
<EmailField
key={field.id}
field={field}
recipient={directRecipient}
onSignField={onSignField}
onUnsignField={onUnsignField}
/>
@@ -241,7 +238,6 @@ export const SignDirectTemplateForm = ({
...field,
fieldMeta: parsedFieldMeta,
}}
recipient={directRecipient}
onSignField={onSignField}
onUnsignField={onUnsignField}
/>
@@ -259,7 +255,6 @@ export const SignDirectTemplateForm = ({
...field,
fieldMeta: parsedFieldMeta,
}}
recipient={directRecipient}
onSignField={onSignField}
onUnsignField={onUnsignField}
/>
@@ -277,7 +272,6 @@ export const SignDirectTemplateForm = ({
...field,
fieldMeta: parsedFieldMeta,
}}
recipient={directRecipient}
onSignField={onSignField}
onUnsignField={onUnsignField}
/>
@@ -295,7 +289,6 @@ export const SignDirectTemplateForm = ({
...field,
fieldMeta: parsedFieldMeta,
}}
recipient={directRecipient}
onSignField={onSignField}
onUnsignField={onUnsignField}
/>
@@ -313,7 +306,6 @@ export const SignDirectTemplateForm = ({
...field,
fieldMeta: parsedFieldMeta,
}}
recipient={directRecipient}
onSignField={onSignField}
onUnsignField={onUnsignField}
/>
@@ -351,6 +343,7 @@ export const SignDirectTemplateForm = ({
onChange={(value) => {
setSignature(value);
}}
allowTypedSignature={template.templateMeta?.typedSignatureEnabled}
/>
</CardContent>
</Card>
@@ -383,6 +376,6 @@ export const SignDirectTemplateForm = ({
/>
</div>
</DocumentFlowFormContainerFooter>
</>
</RecipientProvider>
);
};

View File

@@ -1,44 +0,0 @@
'use client';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Download } from 'lucide-react';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import type { Document, DocumentData } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type DocumentDownloadButtonProps = {
document: Pick<Document, 'title'> & {
documentData: DocumentData;
};
};
export const DocumentDownloadButton = ({ document }: DocumentDownloadButtonProps) => {
const { toast } = useToast();
const { _ } = useLingui();
const onDownloadClick = async () => {
try {
if (!document) {
throw new Error('No document available');
}
await downloadPDF({ documentData: document.documentData, fileName: document.title });
} catch (err) {
toast({
title: _(msg`Something went wrong`),
description: _(msg`An error occurred while downloading your document.`),
variant: 'destructive',
});
}
};
return (
<Button className="w-full" onClick={onDownloadClick}>
<Download className="-ml-1 mr-2 inline h-4 w-4" />
<Trans>Download</Trans>
</Button>
);
};

View File

@@ -1,35 +0,0 @@
import React from 'react';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
import { getTeams } from '@documenso/lib/server-only/team/get-teams';
import { Header as AuthenticatedHeader } from '~/components/(dashboard)/layout/header';
import { NextAuthProvider } from '~/providers/next-auth';
export type SigningLayoutProps = {
children: React.ReactNode;
};
export default async function SigningLayout({ children }: SigningLayoutProps) {
await setupI18nSSR();
const { user, session } = await getServerComponentSession();
let teams: TGetTeamsResponse = [];
if (user && session) {
teams = await getTeams({ userId: user.id });
}
return (
<NextAuthProvider session={session}>
<div className="min-h-screen">
{user && <AuthenticatedHeader user={user} teams={teams} />}
<main className="mb-8 mt-8 px-4 md:mb-12 md:mt-12 md:px-8">{children}</main>
</div>
</NextAuthProvider>
);
}

View File

@@ -1,68 +0,0 @@
import { notFound } from 'next/navigation';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getDocumentByAccessToken } from '@documenso/lib/server-only/document/get-document-by-access-token';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { DocumentDownloadButton } from './document-download-button';
export type DocumentAccessPageProps = {
params: {
token?: string;
};
};
export default async function DocumentAccessPage({ params: { token } }: DocumentAccessPageProps) {
await setupI18nSSR();
if (!token) {
return notFound();
}
const { document } = await getDocumentByAccessToken({ token });
const { documentData, documentMeta } = document;
return (
<div className="mx-auto w-full max-w-screen-xl md:px-8">
<h1
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
title={document.title}
>
{document.title}
</h1>
<div className="mt-8 grid grid-cols-12 gap-y-8 lg:gap-x-8 lg:gap-y-0">
<Card
className="col-span-12 rounded-xl before:rounded-xl lg:col-span-7 xl:col-span-8"
gradient
>
<CardContent className="p-2">
<LazyPDFViewer
key={documentData.id}
documentData={documentData}
document={document}
password={documentMeta?.password}
/>
</CardContent>
</Card>
<div className="col-span-12 lg:col-span-5 xl:col-span-4">
<section className="border-border bg-widget flex flex-col rounded-xl border pb-4 pt-6">
<div className="flex flex-row items-center justify-between px-4">
<h3 className="text-foreground text-2xl font-semibold">Download document</h3>
</div>
<p className="text-muted-foreground mt-2 px-4 text-sm">
Download the document as a PDF file.
</p>
<div className="mt-4 border-t px-4 pt-4">
<DocumentDownloadButton document={document} />
</div>
</section>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,73 @@
import { Trans } from '@lingui/macro';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { SigningDisclosure } from '~/components/general/signing-disclosure';
type ConfirmationDialogProps = {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
hasUninsertedFields: boolean;
isSubmitting: boolean;
};
export function AssistantConfirmationDialog({
isOpen,
onClose,
onConfirm,
hasUninsertedFields,
isSubmitting,
}: ConfirmationDialogProps) {
const onOpenChange = () => {
if (isSubmitting) {
return;
}
onClose();
};
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans>Complete Document</Trans>
</DialogTitle>
<DialogDescription>
<Trans>
Are you sure you want to complete the document? This action cannot be undone. Please
ensure that you have completed prefilling all relevant fields before proceeding.
</Trans>
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4">
<SigningDisclosure />
</div>
<DialogFooter className="mt-4">
<Button variant="secondary" onClick={onClose} disabled={isSubmitting}>
Cancel
</Button>
<Button
variant={hasUninsertedFields ? 'destructive' : 'default'}
onClick={onConfirm}
disabled={isSubmitting}
loading={isSubmitting}
>
{isSubmitting ? 'Submitting...' : hasUninsertedFields ? 'Proceed' : 'Continue'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -13,7 +13,6 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
import { fromCheckboxValue, toCheckboxValue } from '@documenso/lib/universal/field-checkbox';
import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -27,29 +26,30 @@ import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredDocumentAuthContext } from './document-auth-provider';
import { useRecipientContext } from './recipient-context';
import { SigningFieldContainer } from './signing-field-container';
export type CheckboxFieldProps = {
field: FieldWithSignatureAndFieldMeta;
recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
};
export const CheckboxField = ({
field,
recipient,
onSignField,
onUnsignField,
}: CheckboxFieldProps) => {
export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFieldProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
const router = useRouter();
const [isPending, startTransition] = useTransition();
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
const parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
const parsedFieldMeta = ZCheckboxFieldMeta.parse(
field.fieldMeta ?? {
type: 'checkbox',
values: [{ id: 1, checked: false, value: '' }],
},
);
const values = parsedFieldMeta.values?.map((item) => ({
...item,
@@ -122,7 +122,9 @@ export const CheckboxField = ({
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
description: isAssistantMode
? _(msg`An error occurred while signing as assistant.`)
: _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
@@ -151,7 +153,7 @@ export const CheckboxField = ({
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the signature.`),
description: _(msg`An error occurred while removing the field.`),
variant: 'destructive',
});
}
@@ -183,28 +185,25 @@ export const CheckboxField = ({
...checkedValues,
item.value.length > 0 ? item.value : `empty-value-${item.id}`,
];
await removeSignedFieldWithToken({
token: recipient.token,
fieldId: field.id,
});
if (isLengthConditionMet) {
await signFieldWithToken({
token: recipient.token,
fieldId: field.id,
value: toCheckboxValue(checkedValues),
isBase64: true,
});
}
} else {
updatedValues = checkedValues.filter(
(v) => v !== item.value && v !== `empty-value-${item.id}`,
);
}
await removeSignedFieldWithToken({
setCheckedValues(updatedValues);
await removeSignedFieldWithToken({
token: recipient.token,
fieldId: field.id,
});
if (updatedValues.length > 0) {
await signFieldWithToken({
token: recipient.token,
fieldId: field.id,
value: toCheckboxValue(updatedValues),
isBase64: true,
});
}
} catch (err) {
@@ -216,7 +215,6 @@ export const CheckboxField = ({
variant: 'destructive',
});
} finally {
setCheckedValues(updatedValues);
startTransition(() => router.refresh());
}
};

View File

@@ -17,7 +17,6 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/tr
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZDateFieldMeta } from '@documenso/lib/types/field-meta';
import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -27,11 +26,11 @@ import type {
import { cn } from '@documenso/ui/lib/utils';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRecipientContext } from './recipient-context';
import { SigningFieldContainer } from './signing-field-container';
export type DateFieldProps = {
field: FieldWithSignature;
recipient: Recipient;
dateFormat?: string | null;
timezone?: string | null;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
@@ -40,17 +39,17 @@ export type DateFieldProps = {
export const DateField = ({
field,
recipient,
dateFormat = DEFAULT_DOCUMENT_DATE_FORMAT,
timezone = DEFAULT_DOCUMENT_TIME_ZONE,
onSignField,
onUnsignField,
}: DateFieldProps) => {
const router = useRouter();
const { _ } = useLingui();
const { toast } = useToast();
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
const [isPending, startTransition] = useTransition();
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
@@ -67,9 +66,7 @@ export const DateField = ({
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
const localDateString = convertToLocalSystemFormat(field.customText, dateFormat, timezone);
const isDifferentTime = field.inserted && localDateString !== field.customText;
const tooltipText = _(
msg`"${field.customText}" will appear on the document as it has a timezone of "${timezone}".`,
);
@@ -102,7 +99,9 @@ export const DateField = ({
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
description: isAssistantMode
? _(msg`An error occurred while signing as assistant.`)
: _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
@@ -128,7 +127,7 @@ export const DateField = ({
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the signature.`),
description: _(msg`An error occurred while removing the field.`),
variant: 'destructive',
});
}

View File

@@ -12,7 +12,6 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/tr
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZDropdownFieldMeta } from '@documenso/lib/types/field-meta';
import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -30,23 +29,19 @@ import {
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredDocumentAuthContext } from './document-auth-provider';
import { useRecipientContext } from './recipient-context';
import { SigningFieldContainer } from './signing-field-container';
export type DropdownFieldProps = {
field: FieldWithSignatureAndFieldMeta;
recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
};
export const DropdownField = ({
field,
recipient,
onSignField,
onUnsignField,
}: DropdownFieldProps) => {
export const DropdownField = ({ field, onSignField, onUnsignField }: DropdownFieldProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
const router = useRouter();
const [isPending, startTransition] = useTransition();
@@ -103,7 +98,9 @@ export const DropdownField = ({
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
description: isAssistantMode
? _(msg`An error occurred while signing as assistant.`)
: _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
@@ -134,7 +131,7 @@ export const DropdownField = ({
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the signature.`),
description: _(msg`An error occurred while removing the field.`),
variant: 'destructive',
});
}

View File

@@ -12,7 +12,6 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/tr
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZEmailFieldMeta } from '@documenso/lib/types/field-meta';
import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -23,22 +22,23 @@ import { cn } from '@documenso/ui/lib/utils';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredSigningContext } from './provider';
import { useRecipientContext } from './recipient-context';
import { SigningFieldContainer } from './signing-field-container';
export type EmailFieldProps = {
field: FieldWithSignature;
recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
};
export const EmailField = ({ field, recipient, onSignField, onUnsignField }: EmailFieldProps) => {
export const EmailField = ({ field, onSignField, onUnsignField }: EmailFieldProps) => {
const router = useRouter();
const { _ } = useLingui();
const { toast } = useToast();
const { email: providedEmail } = useRequiredSigningContext();
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
const [isPending, startTransition] = useTransition();
@@ -86,7 +86,9 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
description: isAssistantMode
? _(msg`An error occurred while signing as assistant.`)
: _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
@@ -112,7 +114,7 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the signature.`),
description: _(msg`An error occurred while removing the field.`),
variant: 'destructive',
});
}

View File

@@ -1,19 +1,22 @@
'use client';
import { useMemo, useState } from 'react';
import { useId, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Trans } from '@lingui/macro';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { useSession } from 'next-auth/react';
import { useForm } from 'react-hook-form';
import { Controller, useForm } from 'react-hook-form';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
import { type Field, FieldType, type Recipient, RecipientRole } from '@documenso/prisma/client';
import type { Recipient } from '@documenso/prisma/client';
import { type Field, FieldType, RecipientRole } from '@documenso/prisma/client';
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
import { trpc } from '@documenso/trpc/react';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { cn } from '@documenso/ui/lib/utils';
@@ -21,8 +24,11 @@ import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { AssistantConfirmationDialog } from './assistant/assistant-confirmation-dialog';
import { useRequiredSigningContext } from './provider';
import { SignDialog } from './sign-dialog';
@@ -32,6 +38,8 @@ export type SigningFormProps = {
fields: Field[];
redirectUrl?: string | null;
isRecipientsTurn: boolean;
allRecipients?: RecipientWithFields[];
setSelectedSignerId?: (id: number | null) => void;
};
export const SigningForm = ({
@@ -40,19 +48,35 @@ export const SigningForm = ({
fields,
redirectUrl,
isRecipientsTurn,
allRecipients = [],
setSelectedSignerId,
}: SigningFormProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const router = useRouter();
const analytics = useAnalytics();
const { data: session } = useSession();
const assistantSignersId = useId();
const { fullName, signature, setFullName, setSignature, signatureValid, setSignatureValid } =
useRequiredSigningContext();
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
const [isConfirmationDialogOpen, setIsConfirmationDialogOpen] = useState(false);
const [isAssistantSubmitting, setIsAssistantSubmitting] = useState(false);
const { mutateAsync: completeDocumentWithToken } =
trpc.recipient.completeDocumentWithToken.useMutation();
const assistantForm = useForm<{ selectedSignerId: number | undefined }>({
defaultValues: {
selectedSignerId: undefined,
},
});
const { handleSubmit, formState } = useForm();
// Keep the loading state going if successful since the redirect may take some time.
@@ -67,7 +91,11 @@ export const SigningForm = ({
const uninsertedFields = useMemo(() => {
return sortFieldsByPosition(fieldsRequiringValidation.filter((field) => !field.inserted));
}, [fields]);
}, [fieldsRequiringValidation]);
const uninsertedRecipientFields = useMemo(() => {
return fieldsRequiringValidation.filter((field) => field.recipientId === recipient.id);
}, [fieldsRequiringValidation, recipient]);
const fieldsValidated = () => {
setValidateUninsertedFields(true);
@@ -88,12 +116,31 @@ export const SigningForm = ({
}
await completeDocument();
};
// Reauth is currently not required for completing the document.
// await executeActionAuthProcedure({
// onReauthFormSubmit: completeDocument,
// actionTarget: 'DOCUMENT',
// });
const onAssistantFormSubmit = () => {
if (uninsertedRecipientFields.length > 0) {
return;
}
setIsConfirmationDialogOpen(true);
};
const handleAssistantConfirmDialogSubmit = async () => {
setIsAssistantSubmitting(true);
try {
await completeDocument();
} catch (err) {
toast({
title: 'Error',
description: 'An error occurred while completing the document. Please try again.',
variant: 'destructive',
});
setIsAssistantSubmitting(false);
setIsConfirmationDialogOpen(false);
}
};
const completeDocument = async (authOptions?: TRecipientActionAuth) => {
@@ -113,7 +160,7 @@ export const SigningForm = ({
};
return (
<form
<div
className={cn(
'dark:bg-background border-border bg-widget sticky flex h-full flex-col rounded-xl border px-4 py-6',
{
@@ -121,7 +168,6 @@ export const SigningForm = ({
'top-4 max-h-[min(68rem,calc(100vh-2rem))]': !session,
},
)}
onSubmit={handleSubmit(onFormSubmit)}
>
{validateUninsertedFields && uninsertedFields[0] && (
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
@@ -129,17 +175,13 @@ export const SigningForm = ({
</FieldToolTip>
)}
<fieldset
disabled={isSubmitting}
className={cn(
'custom-scrollbar -mx-2 flex flex-1 flex-col overflow-y-auto overflow-x-hidden px-2',
)}
>
<div className={cn('flex flex-1 flex-col')}>
<div className="custom-scrollbar -mx-2 flex flex-1 flex-col overflow-y-auto overflow-x-hidden px-2">
<div className="flex flex-1 flex-col">
<h3 className="text-foreground text-2xl font-semibold">
{recipient.role === RecipientRole.VIEWER && <Trans>View Document</Trans>}
{recipient.role === RecipientRole.SIGNER && <Trans>Sign Document</Trans>}
{recipient.role === RecipientRole.APPROVER && <Trans>Approve Document</Trans>}
{recipient.role === RecipientRole.ASSISTANT && <Trans>Assist Document</Trans>}
</h3>
{recipient.role === RecipientRole.VIEWER ? (
@@ -176,91 +218,191 @@ export const SigningForm = ({
</div>
</div>
</>
) : recipient.role === RecipientRole.ASSISTANT ? (
<>
<form onSubmit={assistantForm.handleSubmit(onAssistantFormSubmit)}>
<p className="text-muted-foreground mt-2 text-sm">
<Trans>
Complete the fields for the following signers. Once reviewed, they will inform
you if any modifications are needed.
</Trans>
</p>
<hr className="border-border my-4" />
<fieldset className="dark:bg-background border-border rounded-2xl border bg-white p-3">
<Controller
name="selectedSignerId"
control={assistantForm.control}
rules={{ required: 'Please select a signer' }}
render={({ field }) => (
<RadioGroup
className="gap-0 space-y-3 shadow-none"
value={field.value?.toString()}
onValueChange={(value) => {
field.onChange(value);
setSelectedSignerId?.(Number(value));
}}
>
{allRecipients
.filter((r) => r.fields.length > 0)
.map((r) => (
<div
key={`${assistantSignersId}-${r.id}`}
className="bg-widget border-border relative flex flex-col gap-4 rounded-lg border p-4"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<RadioGroupItem
id={`${assistantSignersId}-${r.id}`}
value={r.id.toString()}
className="after:absolute after:inset-0"
/>
<div className="grid grow gap-1">
<Label
className="inline-flex items-start"
htmlFor={`${assistantSignersId}-${r.id}`}
>
{r.name}
{r.id === recipient.id && (
<span className="text-muted-foreground ml-2">
{_(msg`(You)`)}
</span>
)}
</Label>
<p className="text-muted-foreground text-xs">{r.email}</p>
</div>
</div>
<div className="text-muted-foreground text-xs leading-[inherit]">
{r.fields.length} {r.fields.length === 1 ? 'field' : 'fields'}
</div>
</div>
</div>
))}
</RadioGroup>
)}
/>
</fieldset>
<div className="mt-6 flex flex-col gap-4 md:flex-row">
<Button
type="submit"
className="w-full"
size="lg"
loading={isAssistantSubmitting}
disabled={isAssistantSubmitting || uninsertedRecipientFields.length > 0}
>
{isAssistantSubmitting ? <Trans>Submitting...</Trans> : <Trans>Continue</Trans>}
</Button>
</div>
<AssistantConfirmationDialog
hasUninsertedFields={uninsertedFields.length > 0}
isOpen={isConfirmationDialogOpen}
onClose={() => !isAssistantSubmitting && setIsConfirmationDialogOpen(false)}
onConfirm={handleAssistantConfirmDialogSubmit}
isSubmitting={isAssistantSubmitting}
/>
</form>
</>
) : (
<>
<p className="text-muted-foreground mt-2 text-sm">
<Trans>Please review the document before signing.</Trans>
</p>
<form onSubmit={handleSubmit(onFormSubmit)}>
<p className="text-muted-foreground mt-2 text-sm">
{recipient.role === RecipientRole.APPROVER && !hasSignatureField ? (
<Trans>Please review the document before approving.</Trans>
) : (
<Trans>Please review the document before signing.</Trans>
)}
</p>
<hr className="border-border mb-8 mt-4" />
<hr className="border-border mb-8 mt-4" />
<div className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2">
<div className="flex flex-1 flex-col gap-y-4">
<div>
<Label htmlFor="full-name">
<Trans>Full Name</Trans>
</Label>
<fieldset
disabled={isSubmitting}
className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2"
>
<div className="flex flex-1 flex-col gap-y-4">
<div>
<Label htmlFor="full-name">
<Trans>Full Name</Trans>
</Label>
<Input
type="text"
id="full-name"
className="bg-background mt-2"
value={fullName}
onChange={(e) => setFullName(e.target.value.trimStart())}
/>
</div>
<Input
type="text"
id="full-name"
className="bg-background mt-2"
value={fullName}
onChange={(e) => setFullName(e.target.value.trimStart())}
/>
</div>
<div>
<Label htmlFor="Signature">
<Trans>Signature</Trans>
</Label>
{hasSignatureField && (
<div>
<Label htmlFor="Signature">
<Trans>Signature</Trans>
</Label>
<Card className="mt-2" gradient degrees={-120}>
<CardContent className="p-0">
<SignaturePad
className="h-44 w-full"
disabled={isSubmitting}
defaultValue={signature ?? undefined}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
onChange={(value) => {
if (signatureValid) {
setSignature(value);
}
}}
allowTypedSignature={document.documentMeta?.typedSignatureEnabled}
/>
</CardContent>
</Card>
<Card className="mt-2" gradient degrees={-120}>
<CardContent className="p-0">
<SignaturePad
className="h-44 w-full"
disabled={isSubmitting}
defaultValue={signature ?? undefined}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
onChange={(value) => {
if (signatureValid) {
setSignature(value);
}
}}
allowTypedSignature={document.documentMeta?.typedSignatureEnabled}
/>
</CardContent>
</Card>
{hasSignatureField && !signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>
Signature is too small. Please provide a more complete signature.
</Trans>
{!signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>
Signature is too small. Please provide a more complete signature.
</Trans>
</div>
)}
</div>
)}
</div>
</div>
<div className="flex flex-col gap-4 md:flex-row">
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
variant="secondary"
size="lg"
disabled={typeof window !== 'undefined' && window.history.length <= 1}
onClick={() => router.back()}
>
<Trans>Cancel</Trans>
</Button>
<div className="flex flex-col gap-4 md:flex-row">
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
variant="secondary"
size="lg"
disabled={typeof window !== 'undefined' && window.history.length <= 1}
onClick={() => router.back()}
>
<Trans>Cancel</Trans>
</Button>
<SignDialog
isSubmitting={isSubmitting}
onSignatureComplete={handleSubmit(onFormSubmit)}
documentTitle={document.title}
fields={fields}
fieldsValidated={fieldsValidated}
role={recipient.role}
disabled={!isRecipientsTurn}
/>
</div>
</div>
<SignDialog
isSubmitting={isSubmitting}
onSignatureComplete={handleSubmit(onFormSubmit)}
documentTitle={document.title}
fields={fields}
fieldsValidated={fieldsValidated}
role={recipient.role}
disabled={!isRecipientsTurn}
/>
</div>
</fieldset>
</form>
</>
)}
</div>
</fieldset>
</form>
</div>
</div>
);
};

View File

@@ -12,7 +12,6 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/tr
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -22,26 +21,22 @@ import type {
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredSigningContext } from './provider';
import { useRecipientContext } from './recipient-context';
import { SigningFieldContainer } from './signing-field-container';
export type InitialsFieldProps = {
field: FieldWithSignature;
recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
};
export const InitialsField = ({
field,
recipient,
onSignField,
onUnsignField,
}: InitialsFieldProps) => {
export const InitialsField = ({ field, onSignField, onUnsignField }: InitialsFieldProps) => {
const router = useRouter();
const { toast } = useToast();
const { _ } = useLingui();
const { fullName } = useRequiredSigningContext();
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
const initials = extractInitials(fullName);
const [isPending, startTransition] = useTransition();
@@ -87,7 +82,9 @@ export const InitialsField = ({
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
description: isAssistantMode
? _(msg`An error occurred while signing as assistant.`)
: _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}

View File

@@ -12,7 +12,6 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/tr
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZNameFieldMeta } from '@documenso/lib/types/field-meta';
import { type Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -28,16 +27,16 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredDocumentAuthContext } from './document-auth-provider';
import { useRequiredSigningContext } from './provider';
import { useRecipientContext } from './recipient-context';
import { SigningFieldContainer } from './signing-field-container';
export type NameFieldProps = {
field: FieldWithSignature;
recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
};
export const NameField = ({ field, recipient, onSignField, onUnsignField }: NameFieldProps) => {
export const NameField = ({ field, onSignField, onUnsignField }: NameFieldProps) => {
const router = useRouter();
const { _ } = useLingui();
@@ -45,6 +44,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
const { fullName: providedFullName, setFullName: setProvidedFullName } =
useRequiredSigningContext();
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
@@ -67,7 +67,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
const [localFullName, setLocalFullName] = useState('');
const onPreSign = () => {
if (!providedFullName) {
if (!providedFullName && !isAssistantMode) {
setShowFullNameModal(true);
return false;
}
@@ -90,9 +90,9 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
const onSign = async (authOptions?: TRecipientActionAuth, name?: string) => {
try {
const value = name || providedFullName;
const value = name || providedFullName || '';
if (!value) {
if (!value && !isAssistantMode) {
setShowFullNameModal(true);
return;
}
@@ -124,7 +124,9 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
description: isAssistantMode
? _(msg`An error occurred while signing as assistant.`)
: _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
@@ -150,7 +152,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the signature.`),
description: _(msg`An error occurred while removing the field.`),
variant: 'destructive',
});
}

View File

@@ -13,7 +13,6 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/tr
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZNumberFieldMeta } from '@documenso/lib/types/field-meta';
import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -27,6 +26,7 @@ import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredDocumentAuthContext } from './document-auth-provider';
import { useRecipientContext } from './recipient-context';
import { SigningFieldContainer } from './signing-field-container';
type ValidationErrors = {
@@ -39,18 +39,18 @@ type ValidationErrors = {
export type NumberFieldProps = {
field: FieldWithSignature;
recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
};
export const NumberField = ({ field, recipient, onSignField, onUnsignField }: NumberFieldProps) => {
export const NumberField = ({ field, onSignField, onUnsignField }: NumberFieldProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
const router = useRouter();
const [isPending, startTransition] = useTransition();
const [showRadioModal, setShowRadioModal] = useState(false);
const [showNumberModal, setShowNumberModal] = useState(false);
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
@@ -105,7 +105,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
};
const onDialogSignClick = () => {
setShowRadioModal(false);
setShowNumberModal(false);
void executeActionAuthProcedure({
onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
@@ -148,14 +148,20 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
description: isAssistantMode
? _(msg`An error occurred while signing as assistant.`)
: _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
};
const onPreSign = () => {
setShowRadioModal(true);
if (isAssistantMode) {
return true;
}
setShowNumberModal(true);
if (localNumber && parsedFieldMeta) {
const validationErrors = validateNumberField(localNumber, parsedFieldMeta, true);
@@ -193,18 +199,18 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the signature.`),
description: _(msg`An error occurred while removing the field.`),
variant: 'destructive',
});
}
};
useEffect(() => {
if (!showRadioModal) {
if (!showNumberModal) {
setLocalNumber(parsedFieldMeta?.value ? String(parsedFieldMeta.value) : '0');
setErrors(initialErrors);
}
}, [showRadioModal]);
}, [showNumberModal]);
useEffect(() => {
if (
@@ -222,8 +228,8 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
if (parsedFieldMeta?.label) {
fieldDisplayName =
parsedFieldMeta.label.length > 10
? parsedFieldMeta.label.substring(0, 10) + '...'
parsedFieldMeta.label.length > 20
? parsedFieldMeta.label.substring(0, 20) + '...'
: parsedFieldMeta.label;
}
@@ -235,7 +241,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
onPreSign={onPreSign}
onSign={onSign}
onRemove={onRemove}
type="Signature"
type="Number"
>
{isLoading && (
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
@@ -278,7 +284,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
</div>
)}
<Dialog open={showRadioModal} onOpenChange={setShowRadioModal}>
<Dialog open={showNumberModal} onOpenChange={setShowNumberModal}>
<DialogContent>
<DialogTitle>
{parsedFieldMeta?.label ? parsedFieldMeta?.label : <Trans>Number</Trans>}
@@ -334,7 +340,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
variant="secondary"
onClick={() => {
setShowRadioModal(false);
setShowNumberModal(false);
setLocalNumber('');
}}
>

View File

@@ -12,11 +12,12 @@ import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-f
import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
import { getRecipientsForAssistant } from '@documenso/lib/server-only/recipient/get-recipients-for-assistant';
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { extractNextHeaderRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import { DocumentAuthProvider } from './document-auth-provider';
import { NoLongerAvailable } from './no-longer-available';
@@ -43,14 +44,14 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
const requestMetadata = extractNextHeaderRequestMetadata(requestHeaders);
const [document, fields, recipient, completedFields] = await Promise.all([
const [document, recipient, fields, completedFields] = await Promise.all([
getDocumentAndSenderByToken({
token,
userId: user?.id,
requireAccessAuth: false,
}).catch(() => null),
getFieldsForToken({ token }),
getRecipientByToken({ token }).catch(() => null),
getFieldsForToken({ token }),
getCompletedFieldsForToken({ token }),
]);
@@ -63,12 +64,21 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
return notFound();
}
const recipientWithFields = { ...recipient, fields };
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token });
if (!isRecipientsTurn) {
return redirect(`/sign/${token}/waiting`);
}
const allRecipients =
recipient.role === RecipientRole.ASSISTANT
? await getRecipientsForAssistant({
token,
})
: [];
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
documentAuth: document.authOptions,
recipientAuth: recipient.authOptions,
@@ -153,11 +163,12 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
user={user}
>
<SigningPageView
recipient={recipient}
recipient={recipientWithFields}
document={document}
fields={fields}
completedFields={completedFields}
isRecipientsTurn={isRecipientsTurn}
allRecipients={allRecipients}
/>
</DocumentAuthProvider>
</SigningProvider>

View File

@@ -12,7 +12,6 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/tr
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -24,18 +23,19 @@ import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredDocumentAuthContext } from './document-auth-provider';
import { useRecipientContext } from './recipient-context';
import { SigningFieldContainer } from './signing-field-container';
export type RadioFieldProps = {
field: FieldWithSignatureAndFieldMeta;
recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
};
export const RadioField = ({ field, recipient, onSignField, onUnsignField }: RadioFieldProps) => {
export const RadioField = ({ field, onSignField, onUnsignField }: RadioFieldProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
const router = useRouter();
const [isPending, startTransition] = useTransition();
@@ -99,7 +99,9 @@ export const RadioField = ({ field, recipient, onSignField, onUnsignField }: Rad
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
description: isAssistantMode
? _(msg`An error occurred while signing as assistant.`)
: _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
@@ -126,7 +128,7 @@ export const RadioField = ({ field, recipient, onSignField, onUnsignField }: Rad
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the signature.`),
description: _(msg`An error occurred while removing the selection.`),
variant: 'destructive',
});
}

View File

@@ -0,0 +1,66 @@
'use client';
import { type PropsWithChildren, createContext, useContext } from 'react';
import type { Recipient } from '@documenso/prisma/client';
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
export interface RecipientContextValue {
/**
* The recipient who is currently signing the document.
* In regular mode, this is the actual signer.
* In assistant mode, this is the recipient who is helping fill out the document.
*/
recipient: Recipient | RecipientWithFields;
/**
* Only present in assistant mode.
* The recipient on whose behalf we're filling out the document.
*/
targetSigner: RecipientWithFields | null;
/**
* Whether we're in assistant mode (one recipient filling out for another)
*/
isAssistantMode: boolean;
}
const RecipientContext = createContext<RecipientContextValue | null>(null);
export interface RecipientProviderProps extends PropsWithChildren {
recipient: Recipient | RecipientWithFields;
targetSigner?: RecipientWithFields | null;
}
export const RecipientProvider = ({
children,
recipient,
targetSigner = null,
}: RecipientProviderProps) => {
// console.log({
// recipient,
// targetSigner,
// isAssistantMode: !!targetSigner,
// });
return (
<RecipientContext.Provider
value={{
recipient,
targetSigner,
isAssistantMode: !!targetSigner,
}}
>
{children}
</RecipientContext.Provider>
);
};
export function useRecipientContext() {
const context = useContext(RecipientContext);
if (!context) {
throw new Error('useRecipientContext must be used within a RecipientProvider');
}
return context;
}

View File

@@ -43,9 +43,10 @@ type TRejectDocumentFormSchema = z.infer<typeof ZRejectDocumentFormSchema>;
export interface RejectDocumentDialogProps {
document: Pick<Document, 'id'>;
token: string;
onRejected?: (reason: string) => void | Promise<void>;
}
export function RejectDocumentDialog({ document, token }: RejectDocumentDialogProps) {
export function RejectDocumentDialog({ document, token, onRejected }: RejectDocumentDialogProps) {
const { toast } = useToast();
const router = useRouter();
const searchParams = useSearchParams();
@@ -79,7 +80,11 @@ export function RejectDocumentDialog({ document, token }: RejectDocumentDialogPr
setIsOpen(false);
router.push(`/sign/${token}/rejected`);
if (onRejected) {
await onRejected(reason);
} else {
router.push(`/sign/${token}/rejected`);
}
} catch (err) {
toast({
title: 'Error',

View File

@@ -1,6 +1,7 @@
import { useMemo, useState } from 'react';
import { Trans } from '@lingui/macro';
import { match } from 'ts-pattern';
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
import type { Field } from '@documenso/prisma/client';
@@ -58,62 +59,88 @@ export const SignDialog = ({
loading={isSubmitting}
disabled={disabled}
>
{isComplete ? <Trans>Complete</Trans> : <Trans>Next field</Trans>}
{match({ isComplete, role })
.with({ isComplete: false }, () => <Trans>Next field</Trans>)
.with({ isComplete: true, role: RecipientRole.APPROVER }, () => <Trans>Approve</Trans>)
.with({ isComplete: true, role: RecipientRole.VIEWER }, () => (
<Trans>Mark as viewed</Trans>
))
.with({ isComplete: true }, () => <Trans>Complete</Trans>)
.exhaustive()}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>
<div className="text-foreground text-xl font-semibold">
{role === RecipientRole.VIEWER && <Trans>Complete Viewing</Trans>}
{role === RecipientRole.SIGNER && <Trans>Complete Signing</Trans>}
{role === RecipientRole.APPROVER && <Trans>Complete Approval</Trans>}
{match(role)
.with(RecipientRole.VIEWER, () => <Trans>Complete Viewing</Trans>)
.with(RecipientRole.SIGNER, () => <Trans>Complete Signing</Trans>)
.with(RecipientRole.APPROVER, () => <Trans>Complete Approval</Trans>)
.with(RecipientRole.CC, () => <Trans>Complete Viewing</Trans>)
.with(RecipientRole.ASSISTANT, () => <Trans>Complete Assisting</Trans>)
.exhaustive()}
</div>
</DialogTitle>
<div className="text-muted-foreground max-w-[50ch]">
{role === RecipientRole.VIEWER && (
<span>
<Trans>
<span className="inline-flex flex-wrap">
You are about to complete viewing "
<span className="inline-block max-w-[11rem] truncate align-baseline">
{documentTitle}
{match(role)
.with(RecipientRole.VIEWER, () => (
<span>
<Trans>
<span className="inline-flex flex-wrap">
You are about to complete viewing "
<span className="inline-block max-w-[11rem] truncate align-baseline">
{documentTitle}
</span>
".
</span>
".
</span>
<br /> Are you sure?
</Trans>
</span>
)}
{role === RecipientRole.SIGNER && (
<span>
<Trans>
<span className="inline-flex flex-wrap">
You are about to complete signing "
<span className="inline-block max-w-[11rem] truncate align-baseline">
{documentTitle}
<br /> Are you sure?
</Trans>
</span>
))
.with(RecipientRole.SIGNER, () => (
<span>
<Trans>
<span className="inline-flex flex-wrap">
You are about to complete signing "
<span className="inline-block max-w-[11rem] truncate align-baseline">
{documentTitle}
</span>
".
</span>
".
</span>
<br /> Are you sure?
</Trans>
</span>
)}
{role === RecipientRole.APPROVER && (
<span>
<Trans>
<span className="inline-flex flex-wrap">
You are about to complete approving{' '}
<span className="inline-block max-w-[11rem] truncate align-baseline">
"{documentTitle}"
<br /> Are you sure?
</Trans>
</span>
))
.with(RecipientRole.APPROVER, () => (
<span>
<Trans>
<span className="inline-flex flex-wrap">
You are about to complete approving{' '}
<span className="inline-block max-w-[11rem] truncate align-baseline">
"{documentTitle}"
</span>
.
</span>
.
</span>
<br /> Are you sure?
</Trans>
</span>
)}
<br /> Are you sure?
</Trans>
</span>
))
.otherwise(() => (
<span>
<Trans>
<span className="inline-flex flex-wrap">
You are about to complete viewing "
<span className="inline-block max-w-[11rem] truncate align-baseline">
{documentTitle}
</span>
".
</span>
<br /> Are you sure?
</Trans>
</span>
))}
</div>
<SigningDisclosure className="mt-4" />
@@ -138,9 +165,13 @@ export const SignDialog = ({
loading={isSubmitting}
onClick={onSignatureComplete}
>
{role === RecipientRole.VIEWER && <Trans>Mark as Viewed</Trans>}
{role === RecipientRole.SIGNER && <Trans>Sign</Trans>}
{role === RecipientRole.APPROVER && <Trans>Approve</Trans>}
{match(role)
.with(RecipientRole.VIEWER, () => <Trans>Mark as Viewed</Trans>)
.with(RecipientRole.SIGNER, () => <Trans>Sign</Trans>)
.with(RecipientRole.APPROVER, () => <Trans>Approve</Trans>)
.with(RecipientRole.CC, () => <Trans>Mark as Viewed</Trans>)
.with(RecipientRole.ASSISTANT, () => <Trans>Complete</Trans>)
.exhaustive()}
</Button>
</div>
</DialogFooter>

View File

@@ -11,7 +11,6 @@ import { Loader } from 'lucide-react';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { type Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -28,12 +27,12 @@ import { SigningDisclosure } from '~/components/general/signing-disclosure';
import { useRequiredDocumentAuthContext } from './document-auth-provider';
import { useRequiredSigningContext } from './provider';
import { useRecipientContext } from './recipient-context';
import { SigningFieldContainer } from './signing-field-container';
type SignatureFieldState = 'empty' | 'signed-image' | 'signed-text';
export type SignatureFieldProps = {
field: FieldWithSignature;
recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
typedSignatureEnabled?: boolean;
@@ -41,15 +40,14 @@ export type SignatureFieldProps = {
export const SignatureField = ({
field,
recipient,
onSignField,
onUnsignField,
typedSignatureEnabled,
}: SignatureFieldProps) => {
const router = useRouter();
const { _ } = useLingui();
const { toast } = useToast();
const { recipient } = useRecipientContext();
const signatureRef = useRef<HTMLParagraphElement>(null);
const containerRef = useRef<HTMLDivElement>(null);

View File

@@ -46,6 +46,7 @@ export type SignatureFieldProps = {
| 'Email'
| 'Name'
| 'Signature'
| 'Text'
| 'Radio'
| 'Dropdown'
| 'Number'
@@ -181,6 +182,23 @@ export const SigningFieldContainer = ({
</button>
)}
{(field.type === FieldType.RADIO || field.type === FieldType.CHECKBOX) &&
field.fieldMeta?.label && (
<div
className={cn(
'absolute -top-16 left-0 right-0 rounded-md p-2 text-center text-xs text-gray-700',
{
'bg-foreground/5 border-border border': !field.inserted,
},
{
'bg-documenso-200 border-primary border': field.inserted,
},
)}
>
{field.fieldMeta.label}
</div>
)}
{children}
</FieldRootContainer>
</div>

View File

@@ -1,3 +1,7 @@
'use client';
import { useState } from 'react';
import { Trans } from '@lingui/macro';
import { match } from 'ts-pattern';
@@ -13,9 +17,10 @@ import {
ZTextFieldMeta,
} from '@documenso/lib/types/field-meta';
import type { CompletedField } from '@documenso/lib/types/fields';
import type { Field, Recipient } from '@documenso/prisma/client';
import type { Field } from '@documenso/prisma/client';
import { FieldType, RecipientRole } from '@documenso/prisma/client';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
@@ -32,16 +37,18 @@ import { InitialsField } from './initials-field';
import { NameField } from './name-field';
import { NumberField } from './number-field';
import { RadioField } from './radio-field';
import { RecipientProvider } from './recipient-context';
import { RejectDocumentDialog } from './reject-document-dialog';
import { SignatureField } from './signature-field';
import { TextField } from './text-field';
export type SigningPageViewProps = {
document: DocumentAndSender;
recipient: Recipient;
recipient: RecipientWithFields;
fields: Field[];
completedFields: CompletedField[];
isRecipientsTurn: boolean;
allRecipients?: RecipientWithFields[];
};
export const SigningPageView = ({
@@ -50,9 +57,12 @@ export const SigningPageView = ({
fields,
completedFields,
isRecipientsTurn,
allRecipients = [],
}: SigningPageViewProps) => {
const { documentData, documentMeta } = document;
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(allRecipients?.[0]?.id);
const shouldUseTeamDetails =
document.teamId && document.team?.teamGlobalSettings?.includeSenderDetails === false;
@@ -64,153 +74,168 @@ export const SigningPageView = ({
senderEmail = document.team?.teamEmail?.email ? `(${document.team.teamEmail.email})` : '';
}
const selectedSigner = allRecipients?.find((r) => r.id === selectedSignerId);
return (
<div className="mx-auto w-full max-w-screen-xl">
<h1
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
title={document.title}
>
{document.title}
</h1>
<div className="mt-2.5 flex flex-wrap items-center justify-between gap-x-6">
<div className="max-w-[50ch]">
<span className="text-muted-foreground truncate" title={senderName}>
{senderName} {senderEmail}
</span>{' '}
<span className="text-muted-foreground">
{match(recipient.role)
.with(RecipientRole.VIEWER, () =>
document.teamId && !shouldUseTeamDetails ? (
<Trans>
on behalf of "{document.team?.name}" has invited you to view this document
</Trans>
) : (
<Trans>has invited you to view this document</Trans>
),
)
.with(RecipientRole.SIGNER, () =>
document.teamId && !shouldUseTeamDetails ? (
<Trans>
on behalf of "{document.team?.name}" has invited you to sign this document
</Trans>
) : (
<Trans>has invited you to sign this document</Trans>
),
)
.with(RecipientRole.APPROVER, () =>
document.teamId && !shouldUseTeamDetails ? (
<Trans>
on behalf of "{document.team?.name}" has invited you to approve this document
</Trans>
) : (
<Trans>has invited you to approve this document</Trans>
),
)
.otherwise(() => null)}
</span>
</div>
<RejectDocumentDialog document={document} token={recipient.token} />
</div>
<div className="mt-8 grid grid-cols-12 gap-y-8 lg:gap-x-8 lg:gap-y-0">
<Card
className="col-span-12 rounded-xl before:rounded-xl lg:col-span-7 xl:col-span-8"
gradient
<RecipientProvider recipient={recipient} targetSigner={selectedSigner ?? null}>
<div className="mx-auto w-full max-w-screen-xl">
<h1
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
title={document.title}
>
<CardContent className="p-2">
<LazyPDFViewer
key={documentData.id}
documentData={documentData}
document={document}
password={documentMeta?.password}
/>
</CardContent>
</Card>
{document.title}
</h1>
<div className="col-span-12 lg:col-span-5 xl:col-span-4">
<SigningForm
document={document}
recipient={recipient}
fields={fields}
redirectUrl={documentMeta?.redirectUrl}
isRecipientsTurn={isRecipientsTurn}
/>
<div className="mt-2.5 flex flex-wrap items-center justify-between gap-x-6">
<div className="max-w-[50ch]">
<span className="text-muted-foreground truncate" title={senderName}>
{senderName} {senderEmail}
</span>{' '}
<span className="text-muted-foreground">
{match(recipient.role)
.with(RecipientRole.VIEWER, () =>
document.teamId && !shouldUseTeamDetails ? (
<Trans>
on behalf of "{document.team?.name}" has invited you to view this document
</Trans>
) : (
<Trans>has invited you to view this document</Trans>
),
)
.with(RecipientRole.SIGNER, () =>
document.teamId && !shouldUseTeamDetails ? (
<Trans>
on behalf of "{document.team?.name}" has invited you to sign this document
</Trans>
) : (
<Trans>has invited you to sign this document</Trans>
),
)
.with(RecipientRole.APPROVER, () =>
document.teamId && !shouldUseTeamDetails ? (
<Trans>
on behalf of "{document.team?.name}" has invited you to approve this document
</Trans>
) : (
<Trans>has invited you to approve this document</Trans>
),
)
.with(RecipientRole.ASSISTANT, () =>
document.teamId && !shouldUseTeamDetails ? (
<Trans>
on behalf of "{document.team?.name}" has invited you to assist this document
</Trans>
) : (
<Trans>has invited you to assist this document</Trans>
),
)
.otherwise(() => null)}
</span>
</div>
<RejectDocumentDialog document={document} token={recipient.token} />
</div>
</div>
<DocumentReadOnlyFields fields={completedFields} />
<AutoSign recipient={recipient} fields={fields} />
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
{fields.map((field) =>
match(field.type)
.with(FieldType.SIGNATURE, () => (
<SignatureField
key={field.id}
field={field}
recipient={recipient}
typedSignatureEnabled={documentMeta?.typedSignatureEnabled}
<div className="mt-8 grid grid-cols-12 gap-y-8 lg:gap-x-8 lg:gap-y-0">
<Card
className="col-span-12 rounded-xl before:rounded-xl lg:col-span-7 xl:col-span-8"
gradient
>
<CardContent className="p-2">
<LazyPDFViewer
key={documentData.id}
documentData={documentData}
document={document}
password={documentMeta?.password}
/>
))
.with(FieldType.INITIALS, () => (
<InitialsField key={field.id} field={field} recipient={recipient} />
))
.with(FieldType.NAME, () => (
<NameField key={field.id} field={field} recipient={recipient} />
))
.with(FieldType.DATE, () => (
<DateField
key={field.id}
field={field}
recipient={recipient}
dateFormat={documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
timezone={documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE}
/>
))
.with(FieldType.EMAIL, () => (
<EmailField key={field.id} field={field} recipient={recipient} />
))
.with(FieldType.TEXT, () => {
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
...field,
fieldMeta: field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : null,
};
return <TextField key={field.id} field={fieldWithMeta} recipient={recipient} />;
})
.with(FieldType.NUMBER, () => {
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
...field,
fieldMeta: field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null,
};
return <NumberField key={field.id} field={fieldWithMeta} recipient={recipient} />;
})
.with(FieldType.RADIO, () => {
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
...field,
fieldMeta: field.fieldMeta ? ZRadioFieldMeta.parse(field.fieldMeta) : null,
};
return <RadioField key={field.id} field={fieldWithMeta} recipient={recipient} />;
})
.with(FieldType.CHECKBOX, () => {
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
...field,
fieldMeta: field.fieldMeta ? ZCheckboxFieldMeta.parse(field.fieldMeta) : null,
};
return <CheckboxField key={field.id} field={fieldWithMeta} recipient={recipient} />;
})
.with(FieldType.DROPDOWN, () => {
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
...field,
fieldMeta: field.fieldMeta ? ZDropdownFieldMeta.parse(field.fieldMeta) : null,
};
return <DropdownField key={field.id} field={fieldWithMeta} recipient={recipient} />;
})
.otherwise(() => null),
</CardContent>
</Card>
<div className="col-span-12 lg:col-span-5 xl:col-span-4">
<SigningForm
document={document}
recipient={recipient}
fields={fields}
redirectUrl={documentMeta?.redirectUrl}
isRecipientsTurn={isRecipientsTurn}
allRecipients={allRecipients}
setSelectedSignerId={setSelectedSignerId}
/>
</div>
</div>
<DocumentReadOnlyFields fields={completedFields} />
{recipient.role !== RecipientRole.ASSISTANT && (
<AutoSign recipient={recipient} fields={fields} />
)}
</ElementVisible>
</div>
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
{fields
.filter(
(field) =>
recipient.role !== RecipientRole.ASSISTANT ||
field.recipientId === selectedSigner?.id,
)
.map((field) =>
match(field.type)
.with(FieldType.SIGNATURE, () => (
<SignatureField
key={field.id}
field={field}
typedSignatureEnabled={documentMeta?.typedSignatureEnabled}
/>
))
.with(FieldType.INITIALS, () => <InitialsField key={field.id} field={field} />)
.with(FieldType.NAME, () => <NameField key={field.id} field={field} />)
.with(FieldType.DATE, () => (
<DateField
key={field.id}
field={field}
dateFormat={documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
timezone={documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE}
/>
))
.with(FieldType.EMAIL, () => <EmailField key={field.id} field={field} />)
.with(FieldType.TEXT, () => {
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
...field,
fieldMeta: field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : null,
};
return <TextField key={field.id} field={fieldWithMeta} />;
})
.with(FieldType.NUMBER, () => {
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
...field,
fieldMeta: field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null,
};
return <NumberField key={field.id} field={fieldWithMeta} />;
})
.with(FieldType.RADIO, () => {
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
...field,
fieldMeta: field.fieldMeta ? ZRadioFieldMeta.parse(field.fieldMeta) : null,
};
return <RadioField key={field.id} field={fieldWithMeta} />;
})
.with(FieldType.CHECKBOX, () => {
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
...field,
fieldMeta: field.fieldMeta ? ZCheckboxFieldMeta.parse(field.fieldMeta) : null,
};
return <CheckboxField key={field.id} field={fieldWithMeta} />;
})
.with(FieldType.DROPDOWN, () => {
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
...field,
fieldMeta: field.fieldMeta ? ZDropdownFieldMeta.parse(field.fieldMeta) : null,
};
return <DropdownField key={field.id} field={fieldWithMeta} />;
})
.otherwise(() => null),
)}
</ElementVisible>
</div>
</RecipientProvider>
);
};

View File

@@ -13,7 +13,6 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/tr
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZTextFieldMeta } from '@documenso/lib/types/field-meta';
import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -27,26 +26,31 @@ import { Textarea } from '@documenso/ui/primitives/textarea';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredDocumentAuthContext } from './document-auth-provider';
import { useRecipientContext } from './recipient-context';
import { SigningFieldContainer } from './signing-field-container';
type ValidationErrors = {
required: string[];
characterLimit: string[];
};
export type TextFieldProps = {
field: FieldWithSignatureAndFieldMeta;
recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
};
export const TextField = ({ field, recipient, onSignField, onUnsignField }: TextFieldProps) => {
export const TextField = ({ field, onSignField, onUnsignField }: TextFieldProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
const router = useRouter();
const initialErrors: Record<string, string[]> = {
const initialErrors: ValidationErrors = {
required: [],
characterLimit: [],
};
const [errors, setErrors] = useState(initialErrors);
const userInputHasErrors = Object.values(errors).some((error) => error.length > 0);
@@ -166,7 +170,9 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
description: isAssistantMode
? _(msg`An error occurred while signing as assistant.`)
: _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
@@ -194,7 +200,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the text.`),
description: _(msg`An error occurred while removing the field.`),
variant: 'destructive',
});
}
@@ -234,7 +240,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
onPreSign={onPreSign}
onSign={onSign}
onRemove={onRemove}
type="Signature"
type="Text"
>
{isLoading && (
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
@@ -276,7 +282,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
>
{field.customText.length < 20
? field.customText
: field.customText.substring(0, 15) + '...'}
: field.customText.substring(0, 20) + '...'}
</p>
</div>
)}

View File

@@ -12,7 +12,7 @@ export type EmbedDocumentCompletedPageProps = {
export const EmbedDocumentCompleted = ({ name, signature }: EmbedDocumentCompletedPageProps) => {
console.log({ signature });
return (
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
<div className="embed--DocumentCompleted relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
<h3 className="text-foreground text-2xl font-semibold">
<Trans>Document Completed!</Trans>
</h3>

View File

@@ -13,6 +13,10 @@ import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import {
isFieldUnsignedAndRequired,
isRequiredField,
} from '@documenso/lib/utils/advanced-fields-helpers';
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { DocumentMeta, Recipient, Signature, TemplateMeta } from '@documenso/prisma/client';
import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client';
@@ -49,7 +53,7 @@ export type EmbedDirectTemplateClientPageProps = {
fields: Field[];
metadata?: DocumentMeta | TemplateMeta | null;
hidePoweredBy?: boolean;
isPlatformOrEnterprise?: boolean;
allowWhiteLabelling?: boolean;
};
export const EmbedDirectTemplateClientPage = ({
@@ -60,7 +64,7 @@ export const EmbedDirectTemplateClientPage = ({
fields,
metadata,
hidePoweredBy = false,
isPlatformOrEnterprise = false,
allowWhiteLabelling = false,
}: EmbedDirectTemplateClientPageProps) => {
const { _ } = useLingui();
const { toast } = useToast();
@@ -94,7 +98,7 @@ export const EmbedDirectTemplateClientPage = ({
const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(() => fields);
const [pendingFields, _completedFields] = [
localFields.filter((field) => !field.inserted),
localFields.filter((field) => isFieldUnsignedAndRequired(field)),
localFields.filter((field) => field.inserted),
];
@@ -112,7 +116,7 @@ export const EmbedDirectTemplateClientPage = ({
const newField: DirectTemplateLocalField = structuredClone({
...field,
customText: payload.value,
customText: payload.value ?? '',
inserted: true,
signedValue: payload,
});
@@ -123,8 +127,10 @@ export const EmbedDirectTemplateClientPage = ({
created: new Date(),
recipientId: 1,
fieldId: 1,
signatureImageAsBase64: payload.value.startsWith('data:') ? payload.value : null,
typedSignature: payload.value.startsWith('data:') ? null : payload.value,
signatureImageAsBase64:
payload.value && payload.value.startsWith('data:') ? payload.value : null,
typedSignature:
payload.value && !payload.value.startsWith('data:') ? payload.value : null,
} satisfies Signature;
}
@@ -182,7 +188,7 @@ export const EmbedDirectTemplateClientPage = ({
};
const onNextFieldClick = () => {
validateFieldsInserted(localFields);
validateFieldsInserted(pendingFields);
setShowPendingFieldTooltip(true);
setIsExpanded(false);
@@ -194,7 +200,7 @@ export const EmbedDirectTemplateClientPage = ({
return;
}
const valid = validateFieldsInserted(localFields);
const valid = validateFieldsInserted(pendingFields);
if (!valid) {
setShowPendingFieldTooltip(true);
@@ -207,12 +213,6 @@ export const EmbedDirectTemplateClientPage = ({
directTemplateExternalId = decodeURIComponent(directTemplateExternalId);
}
localFields.forEach((field) => {
if (!field.signedValue) {
throw new Error('Invalid configuration');
}
});
const {
documentId,
token: documentToken,
@@ -223,13 +223,11 @@ export const EmbedDirectTemplateClientPage = ({
directRecipientName: fullName,
directRecipientEmail: email,
templateUpdatedAt: updatedAt,
signedFieldValues: localFields.map((field) => {
if (!field.signedValue) {
throw new Error('Invalid configuration');
}
return field.signedValue;
}),
signedFieldValues: localFields
.filter((field) => {
return field.signedValue && (isRequiredField(field) || field.inserted);
})
.map((field) => field.signedValue!),
});
if (window.parent) {
@@ -288,7 +286,7 @@ export const EmbedDirectTemplateClientPage = ({
document.documentElement.classList.add('dark-mode-disabled');
}
if (isPlatformOrEnterprise) {
if (allowWhiteLabelling) {
injectCss({
css: data.css,
cssVars: data.cssVars,
@@ -349,7 +347,7 @@ export const EmbedDirectTemplateClientPage = ({
{/* Widget */}
<div
key={isExpanded ? 'expanded' : 'collapsed'}
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
data-expanded={isExpanded || undefined}
>
<div className="border-border bg-widget flex h-fit w-full flex-col rounded-xl border px-4 py-4 md:min-h-[min(calc(100dvh-2rem),48rem)] md:py-6">
@@ -360,19 +358,34 @@ export const EmbedDirectTemplateClientPage = ({
<Trans>Sign document</Trans>
</h3>
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
{isExpanded ? (
<LucideChevronDown
className="text-muted-foreground h-5 w-5"
onClick={() => setIsExpanded(false)}
/>
) : (
<LucideChevronUp
className="text-muted-foreground h-5 w-5"
onClick={() => setIsExpanded(true)}
/>
)}
</Button>
{isExpanded ? (
<Button
variant="outline"
className="h-8 w-8 p-0 md:hidden"
onClick={() => setIsExpanded(false)}
>
<LucideChevronDown className="text-muted-foreground h-5 w-5" />
</Button>
) : pendingFields.length > 0 ? (
<Button
variant="outline"
className="h-8 w-8 p-0 md:hidden"
onClick={() => setIsExpanded(true)}
>
<LucideChevronUp className="text-muted-foreground h-5 w-5" />
</Button>
) : (
<Button
variant="default"
size="sm"
className="md:hidden"
disabled={isThrottled || (hasSignatureField && !signatureValid)}
loading={isSubmitting}
onClick={() => throttledOnCompleteClick()}
>
<Trans>Complete</Trans>
</Button>
)}
</div>
</div>
@@ -417,40 +430,42 @@ export const EmbedDirectTemplateClientPage = ({
/>
</div>
<div>
<Label htmlFor="Signature">
<Trans>Signature</Trans>
</Label>
{hasSignatureField && (
<div>
<Label htmlFor="Signature">
<Trans>Signature</Trans>
</Label>
<Card className="mt-2" gradient degrees={-120}>
<CardContent className="p-0">
<SignaturePad
className="h-44 w-full"
disabled={isThrottled || isSubmitting}
defaultValue={signature ?? undefined}
onChange={(value) => {
setSignature(value);
}}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
allowTypedSignature={Boolean(
metadata &&
'typedSignatureEnabled' in metadata &&
metadata.typedSignatureEnabled,
)}
/>
</CardContent>
</Card>
<Card className="mt-2" gradient degrees={-120}>
<CardContent className="p-0">
<SignaturePad
className="h-44 w-full"
disabled={isThrottled || isSubmitting}
defaultValue={signature ?? undefined}
onChange={(value) => {
setSignature(value);
}}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
allowTypedSignature={Boolean(
metadata &&
'typedSignatureEnabled' in metadata &&
metadata.typedSignatureEnabled,
)}
/>
</CardContent>
</Card>
{hasSignatureField && !signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>
Signature is too small. Please provide a more complete signature.
</Trans>
</div>
)}
</div>
{hasSignatureField && !signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>
Signature is too small. Please provide a more complete signature.
</Trans>
</div>
)}
</div>
)}
</div>
</div>
@@ -485,7 +500,6 @@ export const EmbedDirectTemplateClientPage = ({
{/* Fields */}
<EmbedDocumentFields
recipient={recipient}
fields={localFields}
metadata={metadata}
onSignField={onSignField}

View File

@@ -2,6 +2,7 @@ import { notFound } from 'next/navigation';
import { match } from 'ts-pattern';
import { isCommunityPlan as isUserCommunityPlan } from '@documenso/ee/server-only/util/is-community-plan';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
@@ -13,6 +14,7 @@ import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-provider';
import { SigningProvider } from '~/app/(signing)/sign/[token]/provider';
import { RecipientProvider } from '~/app/(signing)/sign/[token]/recipient-context';
import { EmbedAuthenticateView } from '../../authenticate';
import { EmbedPaywall } from '../../paywall';
@@ -54,12 +56,16 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem
documentAuth: template.authOptions,
});
const [isPlatformDocument, isEnterpriseDocument] = await Promise.all([
const [isPlatformDocument, isEnterpriseDocument, isCommunityPlan] = await Promise.all([
isDocumentPlatform(template),
isUserEnterprise({
userId: template.userId,
teamId: template.teamId ?? undefined,
}),
isUserCommunityPlan({
userId: template.userId,
teamId: template.teamId ?? undefined,
}),
]);
const isAccessAuthValid = match(derivedRecipientAccessAuth)
@@ -96,16 +102,20 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem
recipient={recipient}
user={user}
>
<EmbedDirectTemplateClientPage
token={token}
updatedAt={template.updatedAt}
documentData={template.templateDocumentData}
recipient={recipient}
fields={fields}
metadata={template.templateMeta}
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument}
/>
<RecipientProvider recipient={recipient}>
<EmbedDirectTemplateClientPage
token={token}
updatedAt={template.updatedAt}
documentData={template.templateDocumentData}
recipient={recipient}
fields={fields}
metadata={template.templateMeta}
hidePoweredBy={
isCommunityPlan || isPlatformDocument || isEnterpriseDocument || hidePoweredBy
}
allowWhiteLabelling={isCommunityPlan || isPlatformDocument || isEnterpriseDocument}
/>
</RecipientProvider>
</DocumentAuthProvider>
</SigningProvider>
);

View File

@@ -12,7 +12,7 @@ import {
ZRadioFieldMeta,
ZTextFieldMeta,
} from '@documenso/lib/types/field-meta';
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
import type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client';
import { type Field, FieldType } from '@documenso/prisma/client';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import type {
@@ -33,7 +33,6 @@ import { SignatureField } from '~/app/(signing)/sign/[token]/signature-field';
import { TextField } from '~/app/(signing)/sign/[token]/text-field';
export type EmbedDocumentFieldsProps = {
recipient: Recipient;
fields: Field[];
metadata?: DocumentMeta | TemplateMeta | null;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
@@ -41,7 +40,6 @@ export type EmbedDocumentFieldsProps = {
};
export const EmbedDocumentFields = ({
recipient,
fields,
metadata,
onSignField,
@@ -55,7 +53,6 @@ export const EmbedDocumentFields = ({
<SignatureField
key={field.id}
field={field}
recipient={recipient}
onSignField={onSignField}
onUnsignField={onUnsignField}
typedSignatureEnabled={metadata?.typedSignatureEnabled}
@@ -65,7 +62,6 @@ export const EmbedDocumentFields = ({
<InitialsField
key={field.id}
field={field}
recipient={recipient}
onSignField={onSignField}
onUnsignField={onUnsignField}
/>
@@ -74,7 +70,6 @@ export const EmbedDocumentFields = ({
<NameField
key={field.id}
field={field}
recipient={recipient}
onSignField={onSignField}
onUnsignField={onUnsignField}
/>
@@ -83,7 +78,6 @@ export const EmbedDocumentFields = ({
<DateField
key={field.id}
field={field}
recipient={recipient}
onSignField={onSignField}
onUnsignField={onUnsignField}
dateFormat={metadata?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
@@ -94,7 +88,6 @@ export const EmbedDocumentFields = ({
<EmailField
key={field.id}
field={field}
recipient={recipient}
onSignField={onSignField}
onUnsignField={onUnsignField}
/>
@@ -109,7 +102,6 @@ export const EmbedDocumentFields = ({
<TextField
key={field.id}
field={fieldWithMeta}
recipient={recipient}
onSignField={onSignField}
onUnsignField={onUnsignField}
/>
@@ -125,7 +117,6 @@ export const EmbedDocumentFields = ({
<NumberField
key={field.id}
field={fieldWithMeta}
recipient={recipient}
onSignField={onSignField}
onUnsignField={onUnsignField}
/>
@@ -141,7 +132,6 @@ export const EmbedDocumentFields = ({
<RadioField
key={field.id}
field={fieldWithMeta}
recipient={recipient}
onSignField={onSignField}
onUnsignField={onUnsignField}
/>
@@ -157,7 +147,6 @@ export const EmbedDocumentFields = ({
<CheckboxField
key={field.id}
field={fieldWithMeta}
recipient={recipient}
onSignField={onSignField}
onUnsignField={onUnsignField}
/>
@@ -173,7 +162,6 @@ export const EmbedDocumentFields = ({
<DropdownField
key={field.id}
field={fieldWithMeta}
recipient={recipient}
onSignField={onSignField}
onUnsignField={onUnsignField}
/>

View File

@@ -0,0 +1,40 @@
import { Trans } from '@lingui/macro';
import { XCircle } from 'lucide-react';
import type { Signature } from '@documenso/prisma/client';
export type EmbedDocumentRejectedPageProps = {
name?: string;
signature?: Signature;
};
export const EmbedDocumentRejected = ({ name }: EmbedDocumentRejectedPageProps) => {
return (
<div className="embed--DocumentRejected relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
<div className="flex flex-col items-center">
<div className="flex items-center gap-x-4">
<XCircle className="text-destructive h-10 w-10" />
<h2 className="max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
<Trans>Document Rejected</Trans>
</h2>
</div>
<div className="text-destructive mt-4 flex items-center text-center text-sm">
<Trans>You have rejected this document</Trans>
</div>
<p className="text-muted-foreground mt-6 max-w-[60ch] text-center text-sm">
<Trans>
The document owner has been notified of your decision. They may contact you with further
instructions if necessary.
</Trans>
</p>
<p className="text-muted-foreground mt-2 max-w-[60ch] text-center text-sm">
<Trans>No further action is required from you at this time.</Trans>
</p>
</div>
</div>
);
};

View File

@@ -1,6 +1,6 @@
'use client';
import { useEffect, useLayoutEffect, useState } from 'react';
import { useEffect, useId, useLayoutEffect, useMemo, useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
@@ -8,9 +8,17 @@ import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client';
import type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client';
import {
type DocumentData,
type Field,
FieldType,
RecipientRole,
SigningStatus,
} from '@documenso/prisma/client';
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
import { trpc } from '@documenso/trpc/react';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { Button } from '@documenso/ui/primitives/button';
@@ -19,15 +27,19 @@ import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
import { RecipientProvider } from '~/app/(signing)/sign/[token]/recipient-context';
import { RejectDocumentDialog } from '~/app/(signing)/sign/[token]/reject-document-dialog';
import { Logo } from '~/components/branding/logo';
import { EmbedClientLoading } from '../../client-loading';
import { EmbedDocumentCompleted } from '../../completed';
import { EmbedDocumentFields } from '../../document-fields';
import { EmbedDocumentRejected } from '../../rejected';
import { injectCss } from '../../util';
import { ZSignDocumentEmbedDataSchema } from './schema';
@@ -35,12 +47,13 @@ export type EmbedSignDocumentClientPageProps = {
token: string;
documentId: number;
documentData: DocumentData;
recipient: Recipient;
recipient: RecipientWithFields;
fields: Field[];
metadata?: DocumentMeta | TemplateMeta | null;
isCompleted?: boolean;
hidePoweredBy?: boolean;
isPlatformOrEnterprise?: boolean;
allowWhitelabelling?: boolean;
allRecipients?: RecipientWithFields[];
};
export const EmbedSignDocumentClientPage = ({
@@ -52,7 +65,8 @@ export const EmbedSignDocumentClientPage = ({
metadata,
isCompleted,
hidePoweredBy = false,
isPlatformOrEnterprise = false,
allowWhitelabelling = false,
allRecipients = [],
}: EmbedSignDocumentClientPageProps) => {
const { _ } = useLingui();
const { toast } = useToast();
@@ -70,27 +84,45 @@ export const EmbedSignDocumentClientPage = ({
const [hasFinishedInit, setHasFinishedInit] = useState(false);
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
const [hasRejectedDocument, setHasRejectedDocument] = useState(
recipient.signingStatus === SigningStatus.REJECTED,
);
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(
allRecipients.length > 0 ? allRecipients[0].id : null,
);
const [isExpanded, setIsExpanded] = useState(false);
const [isNameLocked, setIsNameLocked] = useState(false);
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
const [allowDocumentRejection, setAllowDocumentRejection] = useState(false);
const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId);
const isAssistantMode = recipient.role === RecipientRole.ASSISTANT;
const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500);
const [pendingFields, _completedFields] = [
fields.filter((field) => !field.inserted),
fields.filter(
(field) => field.recipientId === recipient.id && isFieldUnsignedAndRequired(field),
),
fields.filter((field) => field.inserted),
];
const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } =
trpc.recipient.completeDocumentWithToken.useMutation();
const fieldsRequiringValidation = useMemo(
() => fields.filter(isFieldUnsignedAndRequired),
[fields],
);
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
const assistantSignersId = useId();
const onNextFieldClick = () => {
validateFieldsInserted(fields);
validateFieldsInserted(fieldsRequiringValidation);
setShowPendingFieldTooltip(true);
setIsExpanded(false);
@@ -102,7 +134,7 @@ export const EmbedSignDocumentClientPage = ({
return;
}
const valid = validateFieldsInserted(fields);
const valid = validateFieldsInserted(fieldsRequiringValidation);
if (!valid) {
setShowPendingFieldTooltip(true);
@@ -150,6 +182,25 @@ export const EmbedSignDocumentClientPage = ({
}
};
const onDocumentRejected = (reason: string) => {
if (window.parent) {
window.parent.postMessage(
{
action: 'document-rejected',
data: {
token,
documentId,
recipientId: recipient.id,
reason,
},
},
'*',
);
}
setHasRejectedDocument(true);
};
useLayoutEffect(() => {
const hash = window.location.hash.slice(1);
@@ -163,12 +214,13 @@ export const EmbedSignDocumentClientPage = ({
// Since a recipient can be provided a name we can lock it without requiring
// a to be provided by the parent application, unlike direct templates.
setIsNameLocked(!!data.lockName);
setAllowDocumentRejection(!!data.allowDocumentRejection);
if (data.darkModeDisabled) {
document.documentElement.classList.add('dark-mode-disabled');
}
if (isPlatformOrEnterprise) {
if (allowWhitelabelling) {
injectCss({
css: data.css,
cssVars: data.cssVars,
@@ -197,6 +249,10 @@ export const EmbedSignDocumentClientPage = ({
}
}, [hasFinishedInit, hasDocumentLoaded]);
if (hasRejectedDocument) {
return <EmbedDocumentRejected name={fullName} />;
}
if (hasCompletedDocument) {
return (
<EmbedDocumentCompleted
@@ -214,164 +270,263 @@ export const EmbedSignDocumentClientPage = ({
}
return (
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
<RecipientProvider recipient={recipient} targetSigner={selectedSigner ?? null}>
<div className="embed--Root relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
<div className="relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
{/* Viewer */}
<div className="flex-1">
<LazyPDFViewer
documentData={documentData}
onDocumentLoad={() => setHasDocumentLoaded(true)}
/>
</div>
{allowDocumentRejection && (
<div className="embed--Actions mb-4 flex w-full flex-row-reverse items-baseline justify-between">
<RejectDocumentDialog
document={{ id: documentId }}
token={token}
onRejected={onDocumentRejected}
/>
</div>
)}
{/* Widget */}
<div
key={isExpanded ? 'expanded' : 'collapsed'}
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
data-expanded={isExpanded || undefined}
>
<div className="border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
{/* Header */}
<div>
<div className="flex items-center justify-between gap-x-2">
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
<Trans>Sign document</Trans>
</h3>
<div className="embed--DocumentContainer relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
{/* Viewer */}
<div className="embed--DocumentViewer flex-1">
<LazyPDFViewer
documentData={documentData}
onDocumentLoad={() => setHasDocumentLoaded(true)}
/>
</div>
{/* Widget */}
<div
key={isExpanded ? 'expanded' : 'collapsed'}
className="embed--DocumentWidgetContainer group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
data-expanded={isExpanded || undefined}
>
<div className="embed--DocumentWidget border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
{/* Header */}
<div className="embed--DocumentWidgetHeader">
<div className="flex items-center justify-between gap-x-2">
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
{isAssistantMode ? (
<Trans>Assist with signing</Trans>
) : (
<Trans>Sign document</Trans>
)}
</h3>
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
{isExpanded ? (
<LucideChevronDown
className="text-muted-foreground h-5 w-5"
<Button
variant="outline"
className="bg-background dark:bg-foreground h-8 w-8 p-0 md:hidden"
onClick={() => setIsExpanded(false)}
/>
) : (
<LucideChevronUp
className="text-muted-foreground h-5 w-5"
>
<LucideChevronDown className="text-muted-foreground dark:text-background h-5 w-5" />
</Button>
) : pendingFields.length > 0 ? (
<Button
variant="outline"
className="bg-background dark:bg-foreground h-8 w-8 p-0 md:hidden"
onClick={() => setIsExpanded(true)}
/>
>
<LucideChevronUp className="text-muted-foreground dark:text-background h-5 w-5" />
</Button>
) : (
<Button
variant="default"
size="sm"
className="md:hidden"
disabled={
isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
}
loading={isSubmitting}
onClick={() => throttledOnCompleteClick()}
>
<Trans>Complete</Trans>
</Button>
)}
</Button>
</div>
</div>
</div>
<div className="hidden group-data-[expanded]/document-widget:block md:block">
<p className="text-muted-foreground mt-2 text-sm">
<Trans>Sign the document to complete the process.</Trans>
</p>
<div className="embed--DocumentWidgetContent hidden group-data-[expanded]/document-widget:block md:block">
<p className="text-muted-foreground mt-2 text-sm">
{isAssistantMode ? (
<Trans>Help complete the document for other signers.</Trans>
) : (
<Trans>Sign the document to complete the process.</Trans>
)}
</p>
<hr className="border-border mb-8 mt-4" />
</div>
<hr className="border-border mb-8 mt-4" />
</div>
{/* Form */}
<div className="-mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
<div className="flex flex-1 flex-col gap-y-4">
<div>
<Label htmlFor="full-name">
<Trans>Full Name</Trans>
</Label>
{/* Form */}
<div className="embed--DocumentWidgetForm -mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
<div className="flex flex-1 flex-col gap-y-4">
{isAssistantMode && (
<div>
<Label>
<Trans>Signing for</Trans>
</Label>
<Input
type="text"
id="full-name"
className="bg-background mt-2"
disabled={isNameLocked}
value={fullName}
onChange={(e) => !isNameLocked && setFullName(e.target.value)}
/>
</div>
<fieldset className="dark:bg-background border-border mt-2 rounded-2xl border bg-white p-3">
<RadioGroup
className="gap-0 space-y-3 shadow-none"
value={selectedSignerId?.toString()}
onValueChange={(value) => setSelectedSignerId(Number(value))}
>
{allRecipients
.filter((r) => r.fields.length > 0)
.map((r) => (
<div
key={`${assistantSignersId}-${r.id}`}
className="bg-widget border-border relative flex flex-col gap-4 rounded-lg border p-4"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<RadioGroupItem
id={`${assistantSignersId}-${r.id}`}
value={r.id.toString()}
className="after:absolute after:inset-0"
/>
<div>
<Label htmlFor="email">
<Trans>Email</Trans>
</Label>
<div className="grid grow gap-1">
<Label
className="inline-flex items-start"
htmlFor={`${assistantSignersId}-${r.id}`}
>
{r.name}
<Input
type="email"
id="email"
className="bg-background mt-2"
value={email}
disabled
/>
</div>
<div>
<Label htmlFor="Signature">
<Trans>Signature</Trans>
</Label>
<Card className="mt-2" gradient degrees={-120}>
<CardContent className="p-0">
<SignaturePad
className="h-44 w-full"
disabled={isThrottled || isSubmitting}
defaultValue={signature ?? undefined}
onChange={(value) => {
setSignature(value);
}}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
allowTypedSignature={Boolean(
metadata &&
'typedSignatureEnabled' in metadata &&
metadata.typedSignatureEnabled,
)}
/>
</CardContent>
</Card>
{hasSignatureField && !signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>
Signature is too small. Please provide a more complete signature.
</Trans>
{r.id === recipient.id && (
<span className="text-muted-foreground ml-2">
{_(msg`(You)`)}
</span>
)}
</Label>
<p className="text-muted-foreground text-xs">{r.email}</p>
</div>
</div>
<div className="text-muted-foreground text-xs leading-[inherit]">
{r.fields.length} {r.fields.length === 1 ? 'field' : 'fields'}
</div>
</div>
</div>
))}
</RadioGroup>
</fieldset>
</div>
)}
{!isAssistantMode && (
<>
<div>
<Label htmlFor="full-name">
<Trans>Full Name</Trans>
</Label>
<Input
type="text"
id="full-name"
className="bg-background mt-2"
disabled={isNameLocked}
value={fullName}
onChange={(e) => !isNameLocked && setFullName(e.target.value)}
/>
</div>
<div>
<Label htmlFor="email">
<Trans>Email</Trans>
</Label>
<Input
type="email"
id="email"
className="bg-background mt-2"
value={email}
disabled
/>
</div>
{hasSignatureField && (
<div>
<Label htmlFor="Signature">
<Trans>Signature</Trans>
</Label>
<Card className="mt-2" gradient degrees={-120}>
<CardContent className="p-0">
<SignaturePad
className="h-44 w-full"
disabled={isThrottled || isSubmitting}
defaultValue={signature ?? undefined}
onChange={(value) => {
setSignature(value);
}}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
allowTypedSignature={Boolean(
metadata &&
'typedSignatureEnabled' in metadata &&
metadata.typedSignatureEnabled,
)}
/>
</CardContent>
</Card>
{hasSignatureField && !signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>
Signature is too small. Please provide a more complete signature.
</Trans>
</div>
)}
</div>
)}
</>
)}
</div>
</div>
</div>
<div className="hidden flex-1 group-data-[expanded]/document-widget:block md:block" />
<div className="hidden flex-1 group-data-[expanded]/document-widget:block md:block" />
<div className="mt-4 hidden w-full grid-cols-2 items-center group-data-[expanded]/document-widget:grid md:grid">
{pendingFields.length > 0 ? (
<Button className="col-start-2" onClick={() => onNextFieldClick()}>
<Trans>Next</Trans>
</Button>
) : (
<Button
className="col-start-2"
disabled={isThrottled || (hasSignatureField && !signatureValid)}
loading={isSubmitting}
onClick={() => throttledOnCompleteClick()}
>
<Trans>Complete</Trans>
</Button>
)}
<div className="embed--DocumentWidgetFooter mt-4 hidden w-full grid-cols-2 items-center group-data-[expanded]/document-widget:grid md:grid">
{pendingFields.length > 0 ? (
<Button className="col-start-2" onClick={() => onNextFieldClick()}>
<Trans>Next</Trans>
</Button>
) : (
<Button
className={allowDocumentRejection ? 'col-start-2' : 'col-span-2'}
disabled={
isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
}
loading={isSubmitting}
onClick={() => throttledOnCompleteClick()}
>
<Trans>Complete</Trans>
</Button>
)}
</div>
</div>
</div>
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
{showPendingFieldTooltip && pendingFields.length > 0 && (
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning">
<Trans>Click to insert field</Trans>
</FieldToolTip>
)}
</ElementVisible>
{/* Fields */}
<EmbedDocumentFields fields={fields} metadata={metadata} />
</div>
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
{showPendingFieldTooltip && pendingFields.length > 0 && (
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning">
<Trans>Click to insert field</Trans>
</FieldToolTip>
)}
</ElementVisible>
{/* Fields */}
<EmbedDocumentFields recipient={recipient} fields={fields} metadata={metadata} />
{!hidePoweredBy && (
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
<span>Powered by</span>
<Logo className="ml-2 inline-block h-[14px]" />
</div>
)}
</div>
{!hidePoweredBy && (
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
<span>Powered by</span>
<Logo className="ml-2 inline-block h-[14px]" />
</div>
)}
</div>
</RecipientProvider>
);
};

View File

@@ -2,23 +2,27 @@ import { notFound } from 'next/navigation';
import { match } from 'ts-pattern';
import { isCommunityPlan as isUserCommunityPlan } from '@documenso/ee/server-only/util/is-community-plan';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getRecipientsForAssistant } from '@documenso/lib/server-only/recipient/get-recipients-for-assistant';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { DocumentStatus } from '@documenso/prisma/client';
import { DocumentStatus, RecipientRole } from '@documenso/prisma/client';
import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-provider';
import { SigningProvider } from '~/app/(signing)/sign/[token]/provider';
import { EmbedAuthenticateView } from '../../authenticate';
import { EmbedPaywall } from '../../paywall';
import { EmbedWaitingForTurn } from '../../waiting-for-turn';
import { EmbedSignDocumentClientPage } from './client';
export type EmbedSignDocumentPageProps = {
@@ -59,12 +63,16 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
return <EmbedPaywall />;
}
const [isPlatformDocument, isEnterpriseDocument] = await Promise.all([
const [isPlatformDocument, isEnterpriseDocument, isCommunityPlan] = await Promise.all([
isDocumentPlatform(document),
isUserEnterprise({
userId: document.userId,
teamId: document.teamId ?? undefined,
}),
isUserCommunityPlan({
userId: document.userId,
teamId: document.teamId ?? undefined,
}),
]);
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
@@ -85,6 +93,19 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
);
}
const isRecipientsTurnToSign = await getIsRecipientsTurnToSign({ token });
if (!isRecipientsTurnToSign) {
return <EmbedWaitingForTurn />;
}
const allRecipients =
recipient.role === RecipientRole.ASSISTANT
? await getRecipientsForAssistant({
token,
})
: [];
const team = document.teamId
? await getTeamById({ teamId: document.teamId, userId: document.userId }).catch(() => null)
: null;
@@ -110,8 +131,11 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
fields={fields}
metadata={document.documentMeta}
isCompleted={document.status === DocumentStatus.COMPLETED}
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument}
hidePoweredBy={
isCommunityPlan || isPlatformDocument || isEnterpriseDocument || hidePoweredBy
}
allowWhitelabelling={isCommunityPlan || isPlatformDocument || isEnterpriseDocument}
allRecipients={allRecipients}
/>
</DocumentAuthProvider>
</SigningProvider>

View File

@@ -13,4 +13,5 @@ export const ZSignDocumentEmbedDataSchema = ZBaseEmbedDataSchema.extend({
.optional()
.transform((value) => value || undefined),
lockName: z.boolean().optional().default(false),
allowDocumentRejection: z.boolean().optional(),
});

View File

@@ -0,0 +1,48 @@
'use client';
import { useEffect, useState } from 'react';
import { Trans } from '@lingui/macro';
export const EmbedWaitingForTurn = () => {
const [hasPostedMessage, setHasPostedMessage] = useState(false);
useEffect(() => {
if (window.parent && !hasPostedMessage) {
window.parent.postMessage(
{
action: 'document-waiting-for-turn',
data: null,
},
'*',
);
}
setHasPostedMessage(true);
}, [hasPostedMessage]);
if (!hasPostedMessage) {
return null;
}
return (
<div className="embed--WaitingForTurn relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
<h3 className="text-foreground text-center text-2xl font-bold">
<Trans>Waiting for Your Turn</Trans>
</h3>
<div className="mt-8 max-w-[50ch] text-center">
<p className="text-muted-foreground text-sm">
<Trans>
It's currently not your turn to sign. Please check back soon as this document should be
available for you to sign shortly.
</Trans>
</p>
<p className="text-muted-foreground mt-4 text-sm">
<Trans>Please check with the parent application for more information.</Trans>
</p>
</div>
</div>
);
};

View File

@@ -85,7 +85,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
const [search, setSearch] = useState('');
const [pages, setPages] = useState<string[]>([]);
const { data: searchDocumentsData, isLoading: isSearchingDocuments } =
const { data: searchDocumentsData, isPending: isSearchingDocuments } =
trpcReact.document.searchDocuments.useQuery(
{
query: search,

View File

@@ -73,7 +73,7 @@ export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: Deskto
<Button
variant="outline"
className="text-muted-foreground flex w-96 items-center justify-between rounded-lg"
className="text-muted-foreground flex w-full max-w-96 items-center justify-between rounded-lg"
onClick={() => setIsCommandMenuOpen(true)}
>
<div className="flex items-center">
@@ -82,7 +82,7 @@ export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: Deskto
</div>
<div>
<div className="text-muted-foreground bg-muted flex items-center rounded-md px-1.5 py-0.5 text-xs tracking-wider">
<div className="text-muted-foreground bg-muted flex items-center rounded-md px-1.5 py-0.5 text-xs tracking-wider">
{modifierKey}+K
</div>
</div>

View File

@@ -67,7 +67,7 @@ export const TransferTeamDialog = ({
const {
data,
refetch: refetchTeamMembers,
isLoading: loadingTeamMembers,
isPending: loadingTeamMembers,
isLoadingError: loadingTeamMembersError,
} = trpc.team.getTeamMembers.useQuery({
teamId,

View File

@@ -353,6 +353,16 @@ export const DocumentHistorySheet = ({
/>
),
)
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED }, ({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Field prefilled',
value: formatGenericText(data.field.type),
},
]}
/>
))
.exhaustive()}
{isUserDetailsVisible && (

9
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@documenso/root",
"version": "1.9.0-rc.11",
"version": "1.9.1-rc.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@documenso/root",
"version": "1.9.0-rc.11",
"version": "1.9.1-rc.9",
"workspaces": [
"apps/*",
"packages/*"
@@ -106,7 +106,7 @@
},
"apps/web": {
"name": "@documenso/web",
"version": "1.9.0-rc.11",
"version": "1.9.1-rc.9",
"license": "AGPL-3.0",
"dependencies": {
"@documenso/api": "*",
@@ -32565,8 +32565,7 @@
"node_modules/uqr": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.2.tgz",
"integrity": "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==",
"license": "MIT"
"integrity": "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA=="
},
"node_modules/uri-js": {
"version": "4.4.1",

View File

@@ -1,6 +1,6 @@
{
"private": true,
"version": "1.9.0-rc.11",
"version": "1.9.1-rc.9",
"scripts": {
"build": "turbo run build",
"build:web": "turbo run build --filter=@documenso/web",

View File

@@ -586,6 +586,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
userId: user.id,
teamId: team?.id,
recipients: body.recipients,
prefillFields: body.prefillFields,
override: {
title: body.title,
...body.meta,

View File

@@ -11,7 +11,7 @@ import {
ZRecipientActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { ZFieldMetaPrefillFieldsSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import {
DocumentDataType,
DocumentDistributionMethod,
@@ -299,6 +299,7 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
})
.optional(),
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
prefillFields: z.array(ZFieldMetaPrefillFieldsSchema).optional(),
});
export type TGenerateDocumentFromTemplateMutationSchema = z.infer<

View File

@@ -0,0 +1,612 @@
import { expect, test } from '@playwright/test';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import type { TCheckboxFieldMeta, TRadioFieldMeta } from '@documenso/lib/types/field-meta';
import { prisma } from '@documenso/prisma';
import { FieldType, RecipientRole } from '@documenso/prisma/client';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import { apiSignin } from '../../fixtures/authentication';
test.describe('Template Field Prefill API v1', () => {
test('should create a document from template with prefilled fields', async ({
page,
request,
}) => {
// 1. Create a user
const user = await seedUser();
// 2. Create an API token for the user
const { token } = await createApiToken({
userId: user.id,
tokenName: 'test-token',
expiresIn: null,
});
// 3. Create a template with seedBlankTemplate
const template = await seedBlankTemplate(user, {
createTemplateOptions: {
title: 'Template with Advanced Fields',
},
});
// 4. Create a recipient for the template
const recipient = await prisma.recipient.create({
data: {
templateId: template.id,
email: 'recipient@example.com',
name: 'Test Recipient',
role: RecipientRole.SIGNER,
token: 'test-token',
readStatus: 'NOT_OPENED',
sendStatus: 'NOT_SENT',
signingStatus: 'NOT_SIGNED',
},
});
// 5. Add fields to the template
// Add TEXT field
const textField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.TEXT,
page: 1,
positionX: 5,
positionY: 5,
width: 10,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'text',
label: 'Text Field',
},
},
});
// Add NUMBER field
const numberField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.NUMBER,
page: 1,
positionX: 5,
positionY: 15,
width: 10,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'number',
label: 'Number Field',
},
},
});
// Add RADIO field
const radioField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.RADIO,
page: 1,
positionX: 5,
positionY: 25,
width: 10,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'radio',
label: 'Radio Field',
values: [
{ id: 1, value: 'Option A', checked: false },
{ id: 2, value: 'Option B', checked: false },
],
},
},
});
// Add CHECKBOX field
const checkboxField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.CHECKBOX,
page: 1,
positionX: 5,
positionY: 35,
width: 10,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'checkbox',
label: 'Checkbox Field',
values: [
{ id: 1, value: 'Check A', checked: false },
{ id: 2, value: 'Check B', checked: false },
],
},
},
});
// Add DROPDOWN field
const dropdownField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.DROPDOWN,
page: 1,
positionX: 5,
positionY: 45,
width: 10,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'dropdown',
label: 'Dropdown Field',
values: [{ value: 'Select A' }, { value: 'Select B' }],
},
},
});
// 6. Sign in as the user
await apiSignin({
page,
email: user.email,
});
// 7. Navigate to the template
await page.goto(`${WEBAPP_BASE_URL}/templates/${template.id}`);
// 8. Create a document from the template with prefilled fields
const response = await request.post(
`${WEBAPP_BASE_URL}/api/v1/templates/${template.id}/generate-document`,
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
title: 'Document with Prefilled Fields',
recipients: [
{
id: recipient.id,
email: 'recipient@example.com',
name: 'Test Recipient',
role: 'SIGNER',
},
],
prefillFields: [
{
id: textField.id,
type: 'text',
label: 'Prefilled Text',
value: 'This is prefilled text',
},
{
id: numberField.id,
type: 'number',
label: 'Prefilled Number',
value: '42',
},
{
id: radioField.id,
type: 'radio',
label: 'Prefilled Radio',
value: 'Option A',
},
{
id: checkboxField.id,
type: 'checkbox',
label: 'Prefilled Checkbox',
value: ['Check A', 'Check B'],
},
{
id: dropdownField.id,
type: 'dropdown',
label: 'Prefilled Dropdown',
value: 'Select B',
},
],
},
},
);
const responseData = await response.json();
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
expect(responseData.documentId).toBeDefined();
// 9. Verify the document was created with prefilled fields
const document = await prisma.document.findUnique({
where: {
id: responseData.documentId,
},
include: {
fields: true,
},
});
expect(document).not.toBeNull();
// 10. Verify each field has the correct prefilled values
const documentTextField = document?.fields.find(
(field) => field.type === FieldType.TEXT && field.fieldMeta?.type === 'text',
);
expect(documentTextField?.fieldMeta).toMatchObject({
type: 'text',
label: 'Prefilled Text',
text: 'This is prefilled text',
});
const documentNumberField = document?.fields.find(
(field) => field.type === FieldType.NUMBER && field.fieldMeta?.type === 'number',
);
expect(documentNumberField?.fieldMeta).toMatchObject({
type: 'number',
label: 'Prefilled Number',
value: '42',
});
const documentRadioField = document?.fields.find(
(field) => field.type === FieldType.RADIO && field.fieldMeta?.type === 'radio',
);
expect(documentRadioField?.fieldMeta).toMatchObject({
type: 'radio',
label: 'Prefilled Radio',
});
// Check that the correct radio option is selected
const radioValues = (documentRadioField?.fieldMeta as TRadioFieldMeta)?.values || [];
const selectedRadioOption = radioValues.find((option) => option.checked);
expect(selectedRadioOption?.value).toBe('Option A');
const documentCheckboxField = document?.fields.find(
(field) => field.type === FieldType.CHECKBOX && field.fieldMeta?.type === 'checkbox',
);
expect(documentCheckboxField?.fieldMeta).toMatchObject({
type: 'checkbox',
label: 'Prefilled Checkbox',
});
// Check that the correct checkbox options are selected
const checkboxValues = (documentCheckboxField?.fieldMeta as TCheckboxFieldMeta)?.values || [];
const checkedOptions = checkboxValues.filter((option) => option.checked);
expect(checkedOptions.length).toBe(2);
expect(checkedOptions.map((option) => option.value)).toContain('Check A');
expect(checkedOptions.map((option) => option.value)).toContain('Check B');
const documentDropdownField = document?.fields.find(
(field) => field.type === FieldType.DROPDOWN && field.fieldMeta?.type === 'dropdown',
);
expect(documentDropdownField?.fieldMeta).toMatchObject({
type: 'dropdown',
label: 'Prefilled Dropdown',
defaultValue: 'Select B',
});
// 11. Sign in as the recipient and verify the prefilled fields are visible
const documentRecipient = await prisma.recipient.findFirst({
where: {
documentId: document?.id,
email: 'recipient@example.com',
},
});
// Send the document to the recipient
const sendResponse = await request.post(
`${WEBAPP_BASE_URL}/api/v1/documents/${document?.id}/send`,
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
sendEmail: false,
},
},
);
expect(sendResponse.ok()).toBeTruthy();
expect(sendResponse.status()).toBe(200);
expect(documentRecipient).not.toBeNull();
// Visit the signing page
await page.goto(`${WEBAPP_BASE_URL}/sign/${documentRecipient?.token}`);
// Verify the prefilled fields are visible with correct values
// Text field
await expect(page.getByText('This is prefilled')).toBeVisible();
// Number field
await expect(page.getByText('42')).toBeVisible();
// Radio field
await expect(page.getByText('Option A')).toBeVisible();
await expect(page.getByRole('radio', { name: 'Option A' })).toBeChecked();
// Checkbox field
await expect(page.getByText('Check A')).toBeVisible();
await expect(page.getByText('Check B')).toBeVisible();
await expect(page.getByRole('checkbox', { name: 'Check A' })).toBeChecked();
await expect(page.getByRole('checkbox', { name: 'Check B' })).toBeChecked();
// Dropdown field
await expect(page.getByText('Select B')).toBeVisible();
});
test('should create a document from template without prefilled fields', async ({
page,
request,
}) => {
// 1. Create a user
const user = await seedUser();
// 2. Create an API token for the user
const { token } = await createApiToken({
userId: user.id,
tokenName: 'test-token',
expiresIn: null,
});
// 3. Create a template with seedBlankTemplate
const template = await seedBlankTemplate(user, {
createTemplateOptions: {
title: 'Template with Default Fields',
},
});
// 4. Create a recipient for the template
const recipient = await prisma.recipient.create({
data: {
templateId: template.id,
email: 'recipient@example.com',
name: 'Test Recipient',
role: RecipientRole.SIGNER,
token: 'test-token',
readStatus: 'NOT_OPENED',
sendStatus: 'NOT_SENT',
signingStatus: 'NOT_SIGNED',
},
});
// 5. Add fields to the template
// Add TEXT field
const textField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.TEXT,
page: 1,
positionX: 5,
positionY: 5,
width: 10,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'text',
label: 'Default Text Field',
},
},
});
// Add NUMBER field
const numberField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.NUMBER,
page: 1,
positionX: 5,
positionY: 15,
width: 10,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'number',
label: 'Default Number Field',
},
},
});
// 6. Sign in as the user
await apiSignin({
page,
email: user.email,
});
// 7. Navigate to the template
await page.goto(`${WEBAPP_BASE_URL}/templates/${template.id}`);
// 8. Create a document from the template without prefilled fields
const response = await request.post(
`${WEBAPP_BASE_URL}/api/v1/templates/${template.id}/generate-document`,
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
title: 'Document with Default Fields',
recipients: [
{
id: recipient.id,
email: 'recipient@example.com',
name: 'Test Recipient',
role: 'SIGNER',
},
],
},
},
);
const responseData = await response.json();
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
expect(responseData.documentId).toBeDefined();
// 9. Verify the document was created with default fields
const document = await prisma.document.findUnique({
where: {
id: responseData.documentId,
},
include: {
fields: true,
},
});
expect(document).not.toBeNull();
// 10. Verify fields have their default values
const documentTextField = document?.fields.find((field) => field.type === FieldType.TEXT);
expect(documentTextField?.fieldMeta).toMatchObject({
type: 'text',
label: 'Default Text Field',
});
const documentNumberField = document?.fields.find((field) => field.type === FieldType.NUMBER);
expect(documentNumberField?.fieldMeta).toMatchObject({
type: 'number',
label: 'Default Number Field',
});
// 11. Sign in as the recipient and verify the default fields are visible
const documentRecipient = await prisma.recipient.findFirst({
where: {
documentId: document?.id,
email: 'recipient@example.com',
},
});
expect(documentRecipient).not.toBeNull();
const sendResponse = await request.post(
`${WEBAPP_BASE_URL}/api/v1/documents/${document?.id}/send`,
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
sendEmail: false,
},
},
);
expect(sendResponse.ok()).toBeTruthy();
expect(sendResponse.status()).toBe(200);
// Visit the signing page
await page.goto(`${WEBAPP_BASE_URL}/sign/${documentRecipient?.token}`);
// Verify the default fields are visible with correct labels
await expect(page.getByText('Default Text Field')).toBeVisible();
await expect(page.getByText('Default Number Field')).toBeVisible();
});
test('should handle invalid field prefill values', async ({ request }) => {
// 1. Create a user
const user = await seedUser();
// 2. Create an API token for the user
const { token } = await createApiToken({
userId: user.id,
tokenName: 'test-token',
expiresIn: null,
});
// 3. Create a template using seedBlankTemplate
const template = await seedBlankTemplate(user, {
createTemplateOptions: {
title: 'Template for Invalid Test',
visibility: 'EVERYONE',
},
});
// 4. Create a recipient for the template
const recipient = await prisma.recipient.create({
data: {
templateId: template.id,
email: 'recipient@example.com',
name: 'Test Recipient',
role: RecipientRole.SIGNER,
token: 'test-token',
readStatus: 'NOT_OPENED',
sendStatus: 'NOT_SENT',
signingStatus: 'NOT_SIGNED',
},
});
// 5. Add a field to the template
const field = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.RADIO,
page: 1,
positionX: 100,
positionY: 100,
width: 100,
height: 50,
customText: '',
inserted: false,
fieldMeta: {
type: 'radio',
label: 'Radio Field',
values: [
{ id: 1, value: 'Option A', checked: false },
{ id: 2, value: 'Option B', checked: false },
],
},
},
});
// 6. Try to create a document with invalid prefill value
const response = await request.post(
`${WEBAPP_BASE_URL}/api/v1/templates/${template.id}/generate-document`,
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
title: 'Document with Invalid Prefill',
recipients: [
{
id: recipient.id,
email: 'recipient@example.com',
name: 'Test Recipient',
role: 'SIGNER',
},
],
prefillFields: [
{
id: field.id,
type: 'radio',
label: 'Invalid Radio',
value: 'Non-existent Option', // This option doesn't exist
},
],
},
},
);
// 7. Verify the request fails with appropriate error
expect(response.ok()).toBeFalsy();
expect(response.status()).toBe(400);
const errorData = await response.json();
expect(errorData.message).toContain('not found in options for RADIO field');
});
});

View File

@@ -0,0 +1,600 @@
import { expect, test } from '@playwright/test';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import type { TCheckboxFieldMeta, TRadioFieldMeta } from '@documenso/lib/types/field-meta';
import { prisma } from '@documenso/prisma';
import { FieldType, RecipientRole } from '@documenso/prisma/client';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import { apiSignin } from '../../fixtures/authentication';
test.describe('Template Field Prefill API v2', () => {
test('should create a document from template with prefilled fields', async ({
page,
request,
}) => {
// 1. Create a user
const user = await seedUser();
// 2. Create an API token for the user
const { token } = await createApiToken({
userId: user.id,
tokenName: 'test-token',
expiresIn: null,
});
// 3. Create a template with seedBlankTemplate
const template = await seedBlankTemplate(user, {
createTemplateOptions: {
title: 'Template with Advanced Fields V2',
},
});
// 4. Create a recipient for the template
const recipient = await prisma.recipient.create({
data: {
templateId: template.id,
email: 'recipient@example.com',
name: 'Test Recipient',
role: RecipientRole.SIGNER,
token: 'test-token',
readStatus: 'NOT_OPENED',
sendStatus: 'NOT_SENT',
signingStatus: 'NOT_SIGNED',
},
});
// 5. Add fields to the template
// Add TEXT field
const textField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.TEXT,
page: 1,
positionX: 5,
positionY: 5,
width: 20,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'text',
label: 'Text Field',
},
},
});
// Add NUMBER field
const numberField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.NUMBER,
page: 1,
positionX: 5,
positionY: 15,
width: 20,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'number',
label: 'Number Field',
},
},
});
// Add RADIO field
const radioField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.RADIO,
page: 1,
positionX: 5,
positionY: 25,
width: 20,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'radio',
label: 'Radio Field',
values: [
{ id: 1, value: 'Option A', checked: false },
{ id: 2, value: 'Option B', checked: false },
],
},
},
});
// Add CHECKBOX field
const checkboxField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.CHECKBOX,
page: 1,
positionX: 5,
positionY: 35,
width: 20,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'checkbox',
label: 'Checkbox Field',
values: [
{ id: 1, value: 'Check A', checked: false },
{ id: 2, value: 'Check B', checked: false },
],
},
},
});
// Add DROPDOWN field
const dropdownField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.DROPDOWN,
page: 1,
positionX: 5,
positionY: 45,
width: 20,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'dropdown',
label: 'Dropdown Field',
values: [{ value: 'Select A' }, { value: 'Select B' }],
},
},
});
// 6. Sign in as the user
await apiSignin({
page,
email: user.email,
});
// 7. Navigate to the template
await page.goto(`${WEBAPP_BASE_URL}/templates/${template.id}`);
// 8. Create a document from the template with prefilled fields using v2 API
const response = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/use`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
templateId: template.id,
recipients: [
{
id: recipient.id,
email: 'recipient@example.com',
name: 'Test Recipient',
},
],
prefillFields: [
{
id: textField.id,
type: 'text',
label: 'Prefilled Text',
value: 'This is prefilled text',
},
{
id: numberField.id,
type: 'number',
label: 'Prefilled Number',
value: '42',
},
{
id: radioField.id,
type: 'radio',
label: 'Prefilled Radio',
value: 'Option A',
},
{
id: checkboxField.id,
type: 'checkbox',
label: 'Prefilled Checkbox',
value: ['Check A', 'Check B'],
},
{
id: dropdownField.id,
type: 'dropdown',
label: 'Prefilled Dropdown',
value: 'Select B',
},
],
},
});
const responseData = await response.json();
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
expect(responseData.id).toBeDefined();
// 9. Verify the document was created with prefilled fields
const document = await prisma.document.findUnique({
where: {
id: responseData.id,
},
include: {
fields: true,
},
});
expect(document).not.toBeNull();
// 10. Verify each field has the correct prefilled values
const documentTextField = document?.fields.find(
(field) => field.type === FieldType.TEXT && field.fieldMeta?.type === 'text',
);
expect(documentTextField?.fieldMeta).toMatchObject({
type: 'text',
label: 'Prefilled Text',
text: 'This is prefilled text',
});
const documentNumberField = document?.fields.find(
(field) => field.type === FieldType.NUMBER && field.fieldMeta?.type === 'number',
);
expect(documentNumberField?.fieldMeta).toMatchObject({
type: 'number',
label: 'Prefilled Number',
value: '42',
});
const documentRadioField = document?.fields.find(
(field) => field.type === FieldType.RADIO && field.fieldMeta?.type === 'radio',
);
expect(documentRadioField?.fieldMeta).toMatchObject({
type: 'radio',
label: 'Prefilled Radio',
});
// Check that the correct radio option is selected
const radioValues = (documentRadioField?.fieldMeta as TRadioFieldMeta)?.values || [];
const selectedRadioOption = radioValues.find((option) => option.checked);
expect(selectedRadioOption?.value).toBe('Option A');
const documentCheckboxField = document?.fields.find(
(field) => field.type === FieldType.CHECKBOX && field.fieldMeta?.type === 'checkbox',
);
expect(documentCheckboxField?.fieldMeta).toMatchObject({
type: 'checkbox',
label: 'Prefilled Checkbox',
});
// Check that the correct checkbox options are selected
const checkboxValues = (documentCheckboxField?.fieldMeta as TCheckboxFieldMeta)?.values || [];
const checkedOptions = checkboxValues.filter((option) => option.checked);
expect(checkedOptions.length).toBe(2);
expect(checkedOptions.map((option) => option.value)).toContain('Check A');
expect(checkedOptions.map((option) => option.value)).toContain('Check B');
const documentDropdownField = document?.fields.find(
(field) => field.type === FieldType.DROPDOWN && field.fieldMeta?.type === 'dropdown',
);
expect(documentDropdownField?.fieldMeta).toMatchObject({
type: 'dropdown',
label: 'Prefilled Dropdown',
defaultValue: 'Select B',
});
const sendResponse = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/distribute`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
documentId: document?.id,
meta: {
subject: 'Test Subject',
message: 'Test Message',
},
},
});
await expect(sendResponse.ok()).toBeTruthy();
await expect(sendResponse.status()).toBe(200);
// 11. Sign in as the recipient and verify the prefilled fields are visible
const documentRecipient = await prisma.recipient.findFirst({
where: {
documentId: document?.id,
email: 'recipient@example.com',
},
});
expect(documentRecipient).not.toBeNull();
// Visit the signing page
await page.goto(`${WEBAPP_BASE_URL}/sign/${documentRecipient?.token}`);
// Verify the prefilled fields are visible with correct values
// Text field
await expect(page.getByText('This is prefilled')).toBeVisible();
// Number field
await expect(page.getByText('42')).toBeVisible();
// Radio field
await expect(page.getByText('Option A')).toBeVisible();
await expect(page.getByRole('radio', { name: 'Option A' })).toBeChecked();
// Checkbox field
await expect(page.getByText('Check A')).toBeVisible();
await expect(page.getByText('Check B')).toBeVisible();
await expect(page.getByRole('checkbox', { name: 'Check A' })).toBeChecked();
await expect(page.getByRole('checkbox', { name: 'Check B' })).toBeChecked();
// Dropdown field
await expect(page.getByText('Select B')).toBeVisible();
});
test('should create a document from template without prefilled fields', async ({
page,
request,
}) => {
// 1. Create a user
const user = await seedUser();
// 2. Create an API token for the user
const { token } = await createApiToken({
userId: user.id,
tokenName: 'test-token',
expiresIn: null,
});
// 3. Create a template with seedBlankTemplate
const template = await seedBlankTemplate(user, {
createTemplateOptions: {
title: 'Template with Default Fields V2',
},
});
// 4. Create a recipient for the template
const recipient = await prisma.recipient.create({
data: {
templateId: template.id,
email: 'recipient@example.com',
name: 'Test Recipient',
role: RecipientRole.SIGNER,
token: 'test-token',
readStatus: 'NOT_OPENED',
sendStatus: 'NOT_SENT',
signingStatus: 'NOT_SIGNED',
},
});
// 5. Add fields to the template
// Add TEXT field
const textField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.TEXT,
page: 1,
positionX: 5,
positionY: 5,
width: 20,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'text',
label: 'Default Text Field',
},
},
});
// Add NUMBER field
const numberField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.NUMBER,
page: 1,
positionX: 5,
positionY: 15,
width: 20,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'number',
label: 'Default Number Field',
},
},
});
// 6. Sign in as the user
await apiSignin({
page,
email: user.email,
});
// 7. Navigate to the template
await page.goto(`${WEBAPP_BASE_URL}/templates/${template.id}`);
// 8. Create a document from the template without prefilled fields using v2 API
const response = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/use`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
templateId: template.id,
recipients: [
{
id: recipient.id,
email: 'recipient@example.com',
name: 'Test Recipient',
},
],
},
});
const responseData = await response.json();
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
expect(responseData.id).toBeDefined();
// 9. Verify the document was created with default fields
const document = await prisma.document.findUnique({
where: {
id: responseData.id,
},
include: {
fields: true,
},
});
expect(document).not.toBeNull();
// 10. Verify fields have their default values
const documentTextField = document?.fields.find((field) => field.type === FieldType.TEXT);
expect(documentTextField?.fieldMeta).toMatchObject({
type: 'text',
label: 'Default Text Field',
});
const documentNumberField = document?.fields.find((field) => field.type === FieldType.NUMBER);
expect(documentNumberField?.fieldMeta).toMatchObject({
type: 'number',
label: 'Default Number Field',
});
const sendResponse = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/distribute`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
documentId: document?.id,
meta: {
subject: 'Test Subject',
message: 'Test Message',
},
},
});
await expect(sendResponse.ok()).toBeTruthy();
await expect(sendResponse.status()).toBe(200);
// 11. Sign in as the recipient and verify the default fields are visible
const documentRecipient = await prisma.recipient.findFirst({
where: {
documentId: document?.id,
email: 'recipient@example.com',
},
});
expect(documentRecipient).not.toBeNull();
// Visit the signing page
await page.goto(`${WEBAPP_BASE_URL}/sign/${documentRecipient?.token}`);
await expect(page.getByText('This is prefilled')).not.toBeVisible();
});
test('should handle invalid field prefill values', async ({ request }) => {
// 1. Create a user
const user = await seedUser();
// 2. Create an API token for the user
const { token } = await createApiToken({
userId: user.id,
tokenName: 'test-token',
expiresIn: null,
});
// 3. Create a template using seedBlankTemplate
const template = await seedBlankTemplate(user, {
createTemplateOptions: {
title: 'Template for Invalid Test V2',
visibility: 'EVERYONE',
},
});
// 4. Create a recipient for the template
const recipient = await prisma.recipient.create({
data: {
templateId: template.id,
email: 'recipient@example.com',
name: 'Test Recipient',
role: RecipientRole.SIGNER,
token: 'test-token',
readStatus: 'NOT_OPENED',
sendStatus: 'NOT_SENT',
signingStatus: 'NOT_SIGNED',
},
});
// 5. Add a field to the template
const field = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.RADIO,
page: 1,
positionX: 100,
positionY: 100,
width: 100,
height: 50,
customText: '',
inserted: false,
fieldMeta: {
type: 'radio',
label: 'Radio Field',
values: [
{ id: 1, value: 'Option A', checked: false },
{ id: 2, value: 'Option B', checked: false },
],
},
},
});
// 7. Try to create a document with invalid prefill value
const response = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/use`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
templateId: template.id,
recipients: [
{
id: recipient.id,
email: 'recipient@example.com',
name: 'Test Recipient',
},
],
prefillFields: [
{
id: field.id,
type: 'radio',
label: 'Invalid Radio',
value: 'Non-existent Option', // This option doesn't exist
},
],
},
});
// 8. Verify the request fails with appropriate error
expect(response.ok()).toBeFalsy();
expect(response.status()).toBe(400);
const errorData = await response.json();
expect(errorData.message).toContain('not found in options for RADIO field');
});
});

View File

@@ -384,7 +384,9 @@ test('[DOCUMENT_FLOW]: should be able to approve a document', async ({ page }) =
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
}
await page.getByRole('button', { name: 'Complete' }).click();
await page
.getByRole('button', { name: role === RecipientRole.SIGNER ? 'Complete' : 'Approve' })
.click();
await page
.getByRole('button', { name: role === RecipientRole.SIGNER ? 'Sign' : 'Approve' })
.click();
@@ -454,7 +456,7 @@ test('[DOCUMENT_FLOW]: should be able to create, send with redirect url, sign a
const { status } = await getDocumentByToken(token);
expect(status).toBe(DocumentStatus.PENDING);
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Approve' }).click();
await expect(page.getByRole('dialog').getByText('Complete Approval').first()).toBeVisible();
await page.getByRole('button', { name: 'Approve' }).click();
@@ -540,12 +542,19 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip
if (i > 1) {
await page.getByRole('button', { name: 'Add Signer' }).click();
}
await page
.getByPlaceholder('Email')
.getByLabel('Email')
.nth(i - 1)
.focus();
await page
.getByLabel('Email')
.nth(i - 1)
.fill(`user${i}@example.com`);
await page
.getByPlaceholder('Name')
.getByLabel('Name')
.nth(i - 1)
.fill(`User ${i}`);
}

View File

@@ -4,15 +4,14 @@ import { stripe } from '@documenso/lib/server-only/stripe';
type PlanType = (typeof STRIPE_PLAN_TYPE)[keyof typeof STRIPE_PLAN_TYPE];
export const getPricesByPlan = async (plan: PlanType | PlanType[]) => {
const planTypes = typeof plan === 'string' ? [plan] : plan;
const planTypes: string[] = typeof plan === 'string' ? [plan] : plan;
const query = planTypes.map((planType) => `metadata['plan']:'${planType}'`).join(' OR ');
const { data: prices } = await stripe.prices.search({
query,
const prices = await stripe.prices.list({
expand: ['data.product'],
limit: 100,
});
return prices.filter((price) => price.type === 'recurring');
return prices.data.filter(
(price) => price.type === 'recurring' && planTypes.includes(price.metadata.plan),
);
};

View File

@@ -0,0 +1,56 @@
import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
import { prisma } from '@documenso/prisma';
import type { Subscription } from '@documenso/prisma/client';
import { getCommunityPlanPriceIds } from '../stripe/get-community-plan-prices';
export type IsCommunityPlanOptions = {
userId: number;
teamId?: number;
};
/**
* Whether the user or team is on the community plan.
*/
export const isCommunityPlan = async ({
userId,
teamId,
}: IsCommunityPlanOptions): Promise<boolean> => {
let subscriptions: Subscription[] = [];
if (teamId) {
subscriptions = await prisma.team
.findFirstOrThrow({
where: {
id: teamId,
},
select: {
owner: {
include: {
subscriptions: true,
},
},
},
})
.then((team) => team.owner.subscriptions);
} else {
subscriptions = await prisma.user
.findFirstOrThrow({
where: {
id: userId,
},
select: {
subscriptions: true,
},
})
.then((user) => user.subscriptions);
}
if (subscriptions.length === 0) {
return false;
}
const communityPlanPriceIds = await getCommunityPlanPriceIds();
return subscriptionsContainsActivePlan(subscriptions, communityPlanPriceIds);
};

View File

@@ -84,6 +84,9 @@ export const TemplateDocumentInvite = ({
.with(RecipientRole.VIEWER, () => <Trans>Continue by viewing the document.</Trans>)
.with(RecipientRole.APPROVER, () => <Trans>Continue by approving the document.</Trans>)
.with(RecipientRole.CC, () => '')
.with(RecipientRole.ASSISTANT, () => (
<Trans>Continue by assisting with the document.</Trans>
))
.exhaustive()}
</Text>
@@ -104,6 +107,7 @@ export const TemplateDocumentInvite = ({
.with(RecipientRole.VIEWER, () => <Trans>View Document</Trans>)
.with(RecipientRole.APPROVER, () => <Trans>Approve Document</Trans>)
.with(RecipientRole.CC, () => '')
.with(RecipientRole.ASSISTANT, () => <Trans>Assist Document</Trans>)
.exhaustive()}
</Button>
</Section>

View File

@@ -10,6 +10,9 @@ export const DOCUMENT_AUDIT_LOG_EMAIL_FORMAT = {
[DOCUMENT_EMAIL_TYPE.APPROVE_REQUEST]: {
description: 'Approval request',
},
[DOCUMENT_EMAIL_TYPE.ASSISTING_REQUEST]: {
description: 'Assisting request',
},
[DOCUMENT_EMAIL_TYPE.CC]: {
description: 'CC',
},

View File

@@ -32,12 +32,26 @@ export const RECIPIENT_ROLES_DESCRIPTION = {
roleName: msg`Viewer`,
roleNamePlural: msg`Viewers`,
},
[RecipientRole.ASSISTANT]: {
actionVerb: msg`Assist`,
actioned: msg`Assisted`,
progressiveVerb: msg`Assisting`,
roleName: msg`Assistant`,
roleNamePlural: msg`Assistants`,
},
} satisfies Record<keyof typeof RecipientRole, unknown>;
export const RECIPIENT_ROLE_TO_DISPLAY_TYPE = {
[RecipientRole.SIGNER]: `SIGNING_REQUEST`,
[RecipientRole.VIEWER]: `VIEW_REQUEST`,
[RecipientRole.APPROVER]: `APPROVE_REQUEST`,
} as const;
export const RECIPIENT_ROLE_TO_EMAIL_TYPE = {
[RecipientRole.SIGNER]: `SIGNING_REQUEST`,
[RecipientRole.VIEWER]: `VIEW_REQUEST`,
[RecipientRole.APPROVER]: `APPROVE_REQUEST`,
[RecipientRole.ASSISTANT]: `ASSISTING_REQUEST`,
} as const;
export const RECIPIENT_ROLE_SIGNING_REASONS = {
@@ -45,4 +59,5 @@ export const RECIPIENT_ROLE_SIGNING_REASONS = {
[RecipientRole.APPROVER]: msg`I am an approver of this document`,
[RecipientRole.CC]: msg`I am required to receive a copy of this document`,
[RecipientRole.VIEWER]: msg`I am a viewer of this document`,
[RecipientRole.ASSISTANT]: msg`I am an assistant of this document`,
} satisfies Record<keyof typeof RecipientRole, MessageDescriptor>;

View File

@@ -119,21 +119,6 @@ export const run = async ({
documentData.data = documentData.initialData;
}
const existingDocumentAccessToken = await prisma.documentAccessToken.findUnique({
where: {
documentId: document.id,
},
});
if (!existingDocumentAccessToken) {
await prisma.documentAccessToken.create({
data: {
token: nanoid(),
documentId: document.id,
},
});
}
const pdfData = await getFile(documentData);
const certificateData =

View File

@@ -11,7 +11,6 @@ export const getEntireDocument = async ({ id }: GetEntireDocumentOptions) => {
},
include: {
documentMeta: true,
documentAccessToken: true,
user: {
select: {
id: true,

View File

@@ -12,6 +12,7 @@ import {
WebhookTriggerEvents,
} from '@documenso/prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { jobs } from '../../jobs/client';
import type { TRecipientActionAuth } from '../../types/document-auth';
import {
@@ -72,6 +73,13 @@ export const completeDocumentWithToken = async ({
throw new Error(`Recipient ${recipient.id} has already signed`);
}
if (recipient.signingStatus === SigningStatus.REJECTED) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Recipient has already rejected the document',
statusCode: 400,
});
}
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token });

View File

@@ -88,6 +88,7 @@ export const findDocuments = async ({
const searchFilter: Prisma.DocumentWhereInput = {
OR: [
{ title: { contains: query, mode: 'insensitive' } },
{ externalId: { contains: query, mode: 'insensitive' } },
{ recipients: { some: { name: { contains: query, mode: 'insensitive' } } } },
{ recipients: { some: { email: { contains: query, mode: 'insensitive' } } } },
],

View File

@@ -1,39 +0,0 @@
import { prisma } from '@documenso/prisma';
export type GetDocumentByAccessTokenOptions = {
token: string;
};
export const getDocumentByAccessToken = async ({ token }: GetDocumentByAccessTokenOptions) => {
if (!token) {
throw new Error('Missing token');
}
const result = await prisma.documentAccessToken.findFirstOrThrow({
where: {
token,
},
select: {
document: {
select: {
title: true,
documentData: {
select: {
id: true,
type: true,
data: true,
initialData: true,
},
},
documentMeta: {
select: {
password: true,
},
},
},
},
},
});
return result;
};

View File

@@ -14,8 +14,8 @@ import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-reques
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import type { Prisma } from '@documenso/prisma/client';
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import { getI18nInstance } from '../../client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';

View File

@@ -106,21 +106,6 @@ export const sealDocument = async ({
documentData.data = documentData.initialData;
}
const existingDocumentAccessToken = await prisma.documentAccessToken.findUnique({
where: {
documentId: document.id,
},
});
if (!existingDocumentAccessToken) {
await prisma.documentAccessToken.create({
data: {
token: nanoid(),
documentId: document.id,
},
});
}
// !: Need to write the fields onto the document as a hard copy
const pdfData = await getFile(documentData);

View File

@@ -34,6 +34,14 @@ export const searchDocumentsWithKeyword = async ({
userId: userId,
deletedAt: null,
},
{
externalId: {
contains: query,
mode: 'insensitive',
},
userId: userId,
deletedAt: null,
},
{
recipients: {
some: {
@@ -88,6 +96,23 @@ export const searchDocumentsWithKeyword = async ({
},
deletedAt: null,
},
{
externalId: {
contains: query,
mode: 'insensitive',
},
teamId: {
not: null,
},
team: {
members: {
some: {
userId: userId,
},
},
},
deletedAt: null,
},
],
},
include: {

View File

@@ -1,15 +1,55 @@
import { prisma } from '@documenso/prisma';
import { FieldType, RecipientRole, SigningStatus } from '@documenso/prisma/client';
export type GetFieldsForTokenOptions = {
token: string;
};
export const getFieldsForToken = async ({ token }: GetFieldsForTokenOptions) => {
if (!token) {
throw new Error('Missing token');
}
const recipient = await prisma.recipient.findFirst({
where: { token },
});
if (!recipient) {
return [];
}
if (recipient.role === RecipientRole.ASSISTANT) {
return await prisma.field.findMany({
where: {
OR: [
{
type: {
not: FieldType.SIGNATURE,
},
recipient: {
signingStatus: {
not: SigningStatus.SIGNED,
},
signingOrder: {
gte: recipient.signingOrder ?? 0,
},
},
documentId: recipient.documentId,
},
{
recipientId: recipient.id,
},
],
},
include: {
signature: true,
},
});
}
return await prisma.field.findMany({
where: {
recipient: {
token,
},
recipientId: recipient.id,
},
include: {
signature: true,

View File

@@ -4,7 +4,7 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-log
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
export type RemovedSignedFieldWithTokenOptions = {
token: string;
@@ -17,11 +17,28 @@ export const removeSignedFieldWithToken = async ({
fieldId,
requestMetadata,
}: RemovedSignedFieldWithTokenOptions) => {
const recipient = await prisma.recipient.findFirstOrThrow({
where: {
token,
},
});
const field = await prisma.field.findFirstOrThrow({
where: {
id: fieldId,
recipient: {
token,
...(recipient.role !== RecipientRole.ASSISTANT
? {
id: recipient.id,
}
: {
signingOrder: {
gte: recipient.signingOrder ?? 0,
},
signingStatus: {
not: SigningStatus.SIGNED,
},
}),
},
},
include: {
@@ -30,7 +47,7 @@ export const removeSignedFieldWithToken = async ({
},
});
const { document, recipient } = field;
const { document } = field;
if (!document) {
throw new Error(`Document not found for field ${field.id}`);
@@ -40,7 +57,10 @@ export const removeSignedFieldWithToken = async ({
throw new Error(`Document ${document.id} must be pending`);
}
if (recipient?.signingStatus === SigningStatus.SIGNED) {
if (
recipient?.signingStatus === SigningStatus.SIGNED ||
field.recipient.signingStatus === SigningStatus.SIGNED
) {
throw new Error(`Recipient ${recipient.id} has already signed`);
}
@@ -66,20 +86,22 @@ export const removeSignedFieldWithToken = async ({
},
});
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED,
documentId: document.id,
user: {
name: recipient?.name,
email: recipient?.email,
},
requestMetadata,
data: {
field: field.type,
fieldId: field.secondaryId,
},
}),
});
if (recipient.role !== RecipientRole.ASSISTANT) {
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED,
documentId: document.id,
user: {
name: recipient.name,
email: recipient.email,
},
requestMetadata,
data: {
field: field.type,
fieldId: field.secondaryId,
},
}),
});
}
});
};

View File

@@ -10,7 +10,7 @@ import { validateRadioField } from '@documenso/lib/advanced-fields-validation/va
import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
import { fromCheckboxValue } from '@documenso/lib/universal/field-checkbox';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
import { DocumentStatus, FieldType, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../../constants/date-formats';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '../../constants/time-zones';
@@ -56,20 +56,41 @@ export const signFieldWithToken = async ({
authOptions,
requestMetadata,
}: SignFieldWithTokenOptions) => {
const recipient = await prisma.recipient.findFirstOrThrow({
where: {
token,
},
});
const field = await prisma.field.findFirstOrThrow({
where: {
id: fieldId,
recipient: {
token,
...(recipient.role !== RecipientRole.ASSISTANT
? {
id: recipient.id,
}
: {
signingStatus: {
not: SigningStatus.SIGNED,
},
signingOrder: {
gte: recipient.signingOrder ?? 0,
},
}),
},
},
include: {
document: true,
document: {
include: {
recipients: true,
},
},
recipient: true,
},
});
const { document, recipient } = field;
const { document } = field;
if (!document) {
throw new Error(`Document not found for field ${field.id}`);
@@ -87,7 +108,10 @@ export const signFieldWithToken = async ({
throw new Error(`Document ${document.id} must be pending for signing`);
}
if (recipient?.signingStatus === SigningStatus.SIGNED) {
if (
recipient.signingStatus === SigningStatus.SIGNED ||
field.recipient.signingStatus === SigningStatus.SIGNED
) {
throw new Error(`Recipient ${recipient.id} has already signed`);
}
@@ -183,6 +207,8 @@ export const signFieldWithToken = async ({
throw new Error('Typed signatures are not allowed. Please draw your signature');
}
const assistant = recipient.role === RecipientRole.ASSISTANT ? recipient : undefined;
return await prisma.$transaction(async (tx) => {
const updatedField = await tx.field.update({
where: {
@@ -219,11 +245,14 @@ export const signFieldWithToken = async ({
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
type:
assistant && field.recipientId !== assistant.id
? DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED
: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
documentId: document.id,
user: {
email: recipient.email,
name: recipient.name,
email: assistant?.email ?? recipient.email,
name: assistant?.name ?? recipient.name,
},
requestMetadata,
data: {

View File

@@ -9,5 +9,8 @@ export const getRecipientByToken = async ({ token }: GetRecipientByTokenOptions)
where: {
token,
},
include: {
fields: true,
},
});
};

View File

@@ -0,0 +1,57 @@
import { prisma } from '@documenso/prisma';
import { FieldType } from '@documenso/prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
export interface GetRecipientsForAssistantOptions {
token: string;
}
export const getRecipientsForAssistant = async ({ token }: GetRecipientsForAssistantOptions) => {
const assistant = await prisma.recipient.findFirst({
where: {
token,
},
});
if (!assistant) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Assistant not found',
});
}
let recipients = await prisma.recipient.findMany({
where: {
documentId: assistant.documentId,
signingOrder: {
gte: assistant.signingOrder ?? 0,
},
},
include: {
fields: {
where: {
OR: [
{
recipientId: assistant.id,
},
{
type: {
not: FieldType.SIGNATURE,
},
documentId: assistant.documentId,
},
],
},
},
},
});
// Omit the token for recipients other than the assistant so
// it doesn't get sent to the client.
recipients = recipients.map((recipient) => ({
...recipient,
token: recipient.id === assistant.id ? token : '',
}));
return recipients;
};

View File

@@ -37,6 +37,7 @@ import {
mapDocumentToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
import { isRequiredField } from '../../utils/advanced-fields-helpers';
import type { CreateDocumentAuditLogDataResponse } from '../../utils/document-audit-logs';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import {
@@ -175,20 +176,28 @@ export const createDocumentFromDirectTemplate = async ({
const metaSigningOrder = template.templateMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
// Associate, validate and map to a query every direct template recipient field with the provided fields.
// Only process fields that are either required or have been signed by the user
const fieldsToProcess = directTemplateRecipient.fields.filter((templateField) => {
const signedFieldValue = signedFieldValues.find((value) => value.fieldId === templateField.id);
// Include if it's required or has a signed value
return isRequiredField(templateField) || signedFieldValue !== undefined;
});
const createDirectRecipientFieldArgs = await Promise.all(
directTemplateRecipient.fields.map(async (templateField) => {
fieldsToProcess.map(async (templateField) => {
const signedFieldValue = signedFieldValues.find(
(value) => value.fieldId === templateField.id,
);
if (!signedFieldValue) {
if (isRequiredField(templateField) && !signedFieldValue) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid, missing or changed fields',
});
}
if (templateField.type === FieldType.NAME && directRecipientName === undefined) {
directRecipientName === signedFieldValue.value;
directRecipientName === signedFieldValue?.value;
}
const derivedRecipientActionAuth = await validateFieldAuth({
@@ -199,9 +208,18 @@ export const createDocumentFromDirectTemplate = async ({
},
field: templateField,
userId: user?.id,
authOptions: signedFieldValue.authOptions,
authOptions: signedFieldValue?.authOptions,
});
if (!signedFieldValue) {
return {
templateField,
customText: '',
derivedRecipientActionAuth,
signature: null,
};
}
const { value, isBase64 } = signedFieldValue;
const isSignatureField =
@@ -379,7 +397,7 @@ export const createDocumentFromDirectTemplate = async ({
positionY: templateField.positionY,
width: templateField.width,
height: templateField.height,
customText,
customText: customText ?? '',
inserted: true,
fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
})),

View File

@@ -1,3 +1,5 @@
import { match } from 'ts-pattern';
import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import type { DocumentDistributionMethod } from '@documenso/prisma/client';
@@ -17,7 +19,20 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import { ZRecipientAuthOptionsSchema } from '../../types/document-auth';
import type { TDocumentEmailSettings } from '../../types/document-email';
import { ZFieldMetaSchema } from '../../types/field-meta';
import type {
TCheckboxFieldMeta,
TDropdownFieldMeta,
TFieldMetaPrefillFieldsSchema,
TNumberFieldMeta,
TRadioFieldMeta,
TTextFieldMeta,
} from '../../types/field-meta';
import {
ZCheckboxFieldMeta,
ZDropdownFieldMeta,
ZFieldMetaSchema,
ZRadioFieldMeta,
} from '../../types/field-meta';
import {
ZWebhookDocumentSchema,
mapDocumentToWebhookDocumentPayload,
@@ -50,6 +65,7 @@ export type CreateDocumentFromTemplateOptions = {
email: string;
signingOrder?: number | null;
}[];
prefillFields?: TFieldMetaPrefillFieldsSchema[];
customDocumentDataId?: string;
/**
@@ -72,6 +88,165 @@ export type CreateDocumentFromTemplateOptions = {
requestMetadata: ApiRequestMetadata;
};
const getUpdatedFieldMeta = (field: Field, prefillField?: TFieldMetaPrefillFieldsSchema) => {
if (!prefillField) {
return field.fieldMeta;
}
const advancedField = ['NUMBER', 'RADIO', 'CHECKBOX', 'DROPDOWN', 'TEXT'].includes(field.type);
if (!advancedField) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Field ${field.id} is not an advanced field and cannot have field meta information. Allowed types: NUMBER, RADIO, CHECKBOX, DROPDOWN, TEXT.`,
});
}
// We've already validated that the field types match at a higher level
// Start with the existing field meta or an empty object
const existingMeta = field.fieldMeta || {};
// Apply type-specific updates based on the prefill field type using ts-pattern
return match(prefillField)
.with({ type: 'text' }, (field) => {
if (typeof field.value !== 'string') {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Invalid value for TEXT field ${field.id}: expected string, got ${typeof field.value}`,
});
}
const meta: TTextFieldMeta = {
...existingMeta,
type: 'text',
label: field.label,
text: field.value,
};
return meta;
})
.with({ type: 'number' }, (field) => {
if (typeof field.value !== 'string') {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Invalid value for NUMBER field ${field.id}: expected string, got ${typeof field.value}`,
});
}
const meta: TNumberFieldMeta = {
...existingMeta,
type: 'number',
label: field.label,
value: field.value,
};
return meta;
})
.with({ type: 'radio' }, (field) => {
if (typeof field.value !== 'string') {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Invalid value for RADIO field ${field.id}: expected string, got ${typeof field.value}`,
});
}
const result = ZRadioFieldMeta.safeParse(existingMeta);
if (!result.success) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Invalid field meta for RADIO field ${field.id}`,
});
}
const radioMeta = result.data;
// Validate that the value exists in the options
const valueExists = radioMeta.values?.some((option) => option.value === field.value);
if (!valueExists) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Value "${field.value}" not found in options for RADIO field ${field.id}`,
});
}
const newValues = radioMeta.values?.map((option) => ({
...option,
checked: option.value === field.value,
}));
const meta: TRadioFieldMeta = {
...existingMeta,
type: 'radio',
label: field.label,
values: newValues,
};
return meta;
})
.with({ type: 'checkbox' }, (field) => {
const result = ZCheckboxFieldMeta.safeParse(existingMeta);
if (!result.success) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Invalid field meta for CHECKBOX field ${field.id}`,
});
}
const checkboxMeta = result.data;
// Validate that all values exist in the options
for (const value of field.value) {
const valueExists = checkboxMeta.values?.some((option) => option.value === value);
if (!valueExists) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Value "${value}" not found in options for CHECKBOX field ${field.id}`,
});
}
}
const newValues = checkboxMeta.values?.map((option) => ({
...option,
checked: field.value.includes(option.value),
}));
const meta: TCheckboxFieldMeta = {
...existingMeta,
type: 'checkbox',
label: field.label,
values: newValues,
};
return meta;
})
.with({ type: 'dropdown' }, (field) => {
const result = ZDropdownFieldMeta.safeParse(existingMeta);
if (!result.success) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Invalid field meta for DROPDOWN field ${field.id}`,
});
}
const dropdownMeta = result.data;
// Validate that the value exists in the options if values are defined
const valueExists = dropdownMeta.values?.some((option) => option.value === field.value);
if (!valueExists) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Value "${field.value}" not found in options for DROPDOWN field ${field.id}`,
});
}
const meta: TDropdownFieldMeta = {
...existingMeta,
type: 'dropdown',
label: field.label,
defaultValue: field.value,
};
return meta;
})
.otherwise(() => field.fieldMeta);
};
export const createDocumentFromTemplate = async ({
templateId,
externalId,
@@ -81,6 +256,7 @@ export const createDocumentFromTemplate = async ({
customDocumentDataId,
override,
requestMetadata,
prefillFields,
}: CreateDocumentFromTemplateOptions) => {
const template = await prisma.template.findUnique({
where: {
@@ -258,6 +434,47 @@ export const createDocumentFromTemplate = async ({
let fieldsToCreate: Omit<Field, 'id' | 'secondaryId' | 'templateId'>[] = [];
// Get all template field IDs first so we can validate later
const allTemplateFieldIds = finalRecipients.flatMap((recipient) =>
recipient.fields.map((field) => field.id),
);
if (prefillFields?.length) {
// Validate that all prefill field IDs exist in the template
const invalidFieldIds = prefillFields
.map((prefillField) => prefillField.id)
.filter((id) => !allTemplateFieldIds.includes(id));
if (invalidFieldIds.length > 0) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `The following field IDs do not exist in the template: ${invalidFieldIds.join(', ')}`,
});
}
// Validate that all prefill fields have the correct type
for (const prefillField of prefillFields) {
const templateField = finalRecipients
.flatMap((recipient) => recipient.fields)
.find((field) => field.id === prefillField.id);
if (!templateField) {
// This should never happen due to the previous validation, but just in case
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Field with ID ${prefillField.id} not found in the template`,
});
}
const expectedType = templateField.type.toLowerCase();
const actualType = prefillField.type;
if (expectedType !== actualType) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Field type mismatch for field ${prefillField.id}: expected ${expectedType}, got ${actualType}`,
});
}
}
}
Object.values(finalRecipients).forEach(({ email, fields }) => {
const recipient = document.recipients.find((recipient) => recipient.email === email);
@@ -266,19 +483,25 @@ export const createDocumentFromTemplate = async ({
}
fieldsToCreate = fieldsToCreate.concat(
fields.map((field) => ({
documentId: document.id,
recipientId: recipient.id,
type: field.type,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
customText: '',
inserted: false,
fieldMeta: field.fieldMeta,
})),
fields.map((field) => {
const prefillField = prefillFields?.find((value) => value.id === field.id);
// Use type assertion to help TypeScript understand the structure
const updatedFieldMeta = getUpdatedFieldMeta(field, prefillField);
return {
documentId: document.id,
recipientId: recipient.id,
type: field.type,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
customText: '',
inserted: false,
fieldMeta: updatedFieldMeta,
};
}),
);
});

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: de\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-01-19 12:04\n"
"PO-Revision-Date: 2025-01-30 06:04\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -163,7 +163,7 @@ msgstr "{inviterName} hat dich aus dem Dokument<0/>\"{documentName}\" entfernt"
#: packages/email/template-components/template-document-invite.tsx:61
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {0}<0/>\"{documentName}\""
msgstr ""
msgstr "{inviterName} im Namen von \"{teamName}\" hat dich eingeladen, {0}<0/>\"{documentName}\""
#: packages/email/templates/document-invite.tsx:45
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {action} {documentName}"
@@ -308,7 +308,7 @@ msgstr "{signerName} hat das Dokument \"{documentName}\" abgelehnt."
#: packages/email/template-components/template-document-invite.tsx:68
msgid "{teamName} has invited you to {0}<0/>\"{documentName}\""
msgstr ""
msgstr "{teamName} hat dich eingeladen, {0}<0/>\"{documentName}\""
#: packages/email/templates/document-invite.tsx:46
msgid "{teamName} has invited you to {action} {documentName}"
@@ -1232,20 +1232,20 @@ msgstr "Bulk-Import"
#: packages/lib/jobs/definitions/internal/bulk-send-template.handler.ts:203
msgid "Bulk Send Complete: {0}"
msgstr ""
msgstr "Massenversand abgeschlossen: {0}"
#: packages/email/templates/bulk-send-complete.tsx:30
msgid "Bulk send operation complete for template \"{templateName}\""
msgstr ""
msgstr "Massenversand abgeschlossen für Vorlage \"{templateName}\""
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:128
msgid "Bulk Send Template via CSV"
msgstr ""
msgstr "Bulk-Vorlage senden über CSV"
#: apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx:97
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:120
msgid "Bulk Send via CSV"
msgstr ""
msgstr "Massenversand per CSV"
#: packages/email/templates/team-invite.tsx:84
msgid "by <0>{senderName}</0>"
@@ -1788,7 +1788,7 @@ msgstr "Erstellt am {0}"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:143
msgid "CSV Structure"
msgstr ""
msgstr "CSV-Struktur"
#: apps/web/src/components/forms/password.tsx:112
msgid "Current Password"
@@ -1800,7 +1800,7 @@ msgstr "Aktuelles Passwort ist falsch."
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:154
msgid "Current recipients:"
msgstr ""
msgstr "Aktuelle Empfänger:"
#: apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx:28
msgid "Daily"
@@ -2328,7 +2328,7 @@ msgstr "Zertifikat herunterladen"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:168
msgid "Download Template CSV"
msgstr ""
msgstr "Vorlage CSV herunterladen"
#: apps/web/src/app/(dashboard)/templates/[id]/template-page-view-documents-table.tsx:208
#: apps/web/src/components/formatter/document-status.tsx:34
@@ -2659,7 +2659,7 @@ msgstr "Webhook konnte nicht aktualisiert werden"
#: packages/email/templates/bulk-send-complete.tsx:55
msgid "Failed: {failedCount}"
msgstr ""
msgstr "Fehlgeschlagen: {failedCount}"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:100
msgid "Field character limit"
@@ -2721,7 +2721,7 @@ msgstr "Für Fragen zu dieser Offenlegung, elektronischen Unterschriften oder ei
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:147
msgid "For each recipient, provide their email (required) and name (optional) in separate columns. Download the template CSV below for the correct format."
msgstr ""
msgstr "Für jeden Empfänger geben Sie dessen E-Mail (erforderlich) und Namen (optional) in separaten Spalten an. Laden Sie unten die CSV-Vorlage für das korrekte Format herunter."
#: packages/lib/server-only/auth/send-forgot-password.ts:61
msgid "Forgot Password?"
@@ -2834,7 +2834,7 @@ msgstr "Hey, ich bin Timur"
#: packages/email/templates/bulk-send-complete.tsx:36
msgid "Hi {userName},"
msgstr ""
msgstr "Hallo, {userName},"
#: packages/email/templates/reset-password.tsx:56
msgid "Hi, {userName} <0>({userEmail})</0>"
@@ -3238,7 +3238,7 @@ msgstr "Max"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:227
msgid "Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use template defaults."
msgstr ""
msgstr "Maximale Dateigröße: 4MB. Maximal 100 Zeilen pro Upload. Leere Werte verwenden die Vorlagenstandards."
#: packages/lib/constants/teams.ts:12
msgid "Member"
@@ -3868,7 +3868,7 @@ msgstr "Bitte geben Sie <0>{0}</0> ein, um zu bestätigen."
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:172
msgid "Pre-formatted CSV template with example data."
msgstr ""
msgstr "Vorformatiertes CSV-Template mit Beispieldaten."
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:214
#: apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx:58
@@ -4392,7 +4392,7 @@ msgstr "Dokumente im Namen des Teams über die E-Mail-Adresse senden"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:253
msgid "Send documents to recipients immediately"
msgstr ""
msgstr "Dokumente sofort an Empfänger senden"
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:200
msgid "Send on Behalf of Team"
@@ -4864,11 +4864,11 @@ msgstr "Passkey erfolgreich erstellt"
#: packages/email/templates/bulk-send-complete.tsx:52
msgid "Successfully created: {successCount}"
msgstr ""
msgstr "Erfolgreich erstellt: {successCount}"
#: packages/email/templates/bulk-send-complete.tsx:44
msgid "Summary:"
msgstr ""
msgstr "Zusammenfassung:"
#: apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx:57
msgid "System Requirements"
@@ -5089,7 +5089,7 @@ msgstr "Text"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx:140
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:124
msgid "Text Align"
msgstr ""
msgstr "Textausrichtung"
#: apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx:157
msgid "Text Color"
@@ -5177,7 +5177,7 @@ msgstr "Die Ereignisse, die einen Webhook auslösen, der an Ihre URL gesendet wi
#: packages/email/templates/bulk-send-complete.tsx:62
msgid "The following errors occurred:"
msgstr ""
msgstr "Die folgenden Fehler sind aufgetreten:"
#: packages/email/templates/team-delete.tsx:37
msgid "The following team has been deleted by its owner. You will no longer be able to access this team and its documents"
@@ -5637,7 +5637,7 @@ msgstr "Gesamtempfänger"
#: packages/email/templates/bulk-send-complete.tsx:49
msgid "Total rows processed: {totalProcessed}"
msgstr ""
msgstr "Insgesamt verarbeitete Zeilen: {totalProcessed}"
#: apps/web/src/app/(dashboard)/admin/stats/page.tsx:150
msgid "Total Signers that Signed Up"
@@ -5899,7 +5899,7 @@ msgstr "Upgrade"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:132
msgid "Upload a CSV file to create multiple documents from this template. Each row represents one document with its recipient details."
msgstr ""
msgstr "Laden Sie eine CSV-Datei hoch, um mehrere Dokumente aus dieser Vorlage zu erstellen. Jede Zeile repräsentiert ein Dokument mit den Empfängerdaten."
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:426
msgid "Upload a custom document to use instead of the template's default document"
@@ -5907,7 +5907,7 @@ msgstr "Laden Sie ein benutzerdefiniertes Dokument hoch, um es anstelle des Stan
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:267
msgid "Upload and Process"
msgstr ""
msgstr "Hochladen und verarbeiten"
#: apps/web/src/components/forms/avatar-image.tsx:179
msgid "Upload Avatar"
@@ -5915,7 +5915,7 @@ msgstr "Avatar hochladen"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:198
msgid "Upload CSV"
msgstr ""
msgstr "CSV hochladen"
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:419
msgid "Upload custom document"
@@ -6594,7 +6594,7 @@ msgstr "Sie können Dokumente ansehen, die mit dieser E-Mail verknüpft sind, un
#: packages/email/templates/bulk-send-complete.tsx:76
msgid "You can view the created documents in your dashboard under the \"Documents created from template\" section."
msgstr ""
msgstr "Sie können die erstellten Dokumente in Ihrem Dashboard unter der Rubrik \"Dokumente, die aus Vorlage erstellt wurden\" einsehen."
#: packages/email/template-components/template-document-rejected.tsx:37
msgid "You can view the document and its status by clicking the button below."
@@ -6807,11 +6807,11 @@ msgstr "Ihre Markenpräferenzen wurden aktualisiert"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:97
msgid "Your bulk send has been initiated. You will receive an email notification upon completion."
msgstr ""
msgstr "Ihr Massenversand wurde gestartet. Sie erhalten eine E-Mail-Benachrichtigung nach Abschluss."
#: packages/email/templates/bulk-send-complete.tsx:40
msgid "Your bulk send operation for template \"{templateName}\" has completed."
msgstr ""
msgstr "Ihre Massenversandoperation für Vorlage \"{templateName}\" ist abgeschlossen."
#: apps/web/src/app/(dashboard)/settings/billing/page.tsx:125
msgid "Your current plan is past due. Please update your payment information."
@@ -6962,3 +6962,4 @@ msgstr "Ihr Token wurde erfolgreich erstellt! Stellen Sie sicher, dass Sie es ko
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx:86
msgid "Your tokens will be shown here once you create them."
msgstr "Ihre Tokens werden hier angezeigt, sobald Sie sie erstellt haben."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: es\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-01-30 03:57\n"
"PO-Revision-Date: 2025-01-30 06:04\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -163,7 +163,7 @@ msgstr "{inviterName} te ha eliminado del documento<0/>\"{documentName}\""
#: packages/email/template-components/template-document-invite.tsx:61
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {0}<0/>\"{documentName}\""
msgstr ""
msgstr "{inviterName} en nombre de \"{teamName}\" te ha invitado a {0}<0/>\"{documentName}\""
#: packages/email/templates/document-invite.tsx:45
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {action} {documentName}"
@@ -308,7 +308,7 @@ msgstr "{signerName} ha rechazado el documento \"{documentName}\"."
#: packages/email/template-components/template-document-invite.tsx:68
msgid "{teamName} has invited you to {0}<0/>\"{documentName}\""
msgstr ""
msgstr "{teamName} te ha invitado a {0}<0/>\"{documentName}\""
#: packages/email/templates/document-invite.tsx:46
msgid "{teamName} has invited you to {action} {documentName}"
@@ -1232,20 +1232,20 @@ msgstr "Importación masiva"
#: packages/lib/jobs/definitions/internal/bulk-send-template.handler.ts:203
msgid "Bulk Send Complete: {0}"
msgstr ""
msgstr "Envío Masivo Completo: {0}"
#: packages/email/templates/bulk-send-complete.tsx:30
msgid "Bulk send operation complete for template \"{templateName}\""
msgstr ""
msgstr "Operación de envío masivo completa para la plantilla \"{templateName}\""
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:128
msgid "Bulk Send Template via CSV"
msgstr ""
msgstr "Enviar plantilla masiva a través de CSV"
#: apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx:97
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:120
msgid "Bulk Send via CSV"
msgstr ""
msgstr "Envío Masivo vía CSV"
#: packages/email/templates/team-invite.tsx:84
msgid "by <0>{senderName}</0>"
@@ -1338,12 +1338,12 @@ msgstr "No se puede eliminar el firmante"
#: packages/lib/constants/recipient-roles.ts:18
msgid "Cc"
msgstr "Cc"
msgstr "Copia visible"
#: packages/lib/constants/recipient-roles.ts:15
#: packages/lib/constants/recipient-roles.ts:17
msgid "CC"
msgstr "CC"
msgstr "COPIA VISIBLE"
#: packages/lib/constants/recipient-roles.ts:16
msgid "CC'd"
@@ -1788,7 +1788,7 @@ msgstr "Creado el {0}"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:143
msgid "CSV Structure"
msgstr ""
msgstr "Estructura CSV"
#: apps/web/src/components/forms/password.tsx:112
msgid "Current Password"
@@ -1800,7 +1800,7 @@ msgstr "La contraseña actual es incorrecta."
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:154
msgid "Current recipients:"
msgstr ""
msgstr "Destinatarios actuales:"
#: apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx:28
msgid "Daily"
@@ -2328,7 +2328,7 @@ msgstr "Descargar certificado"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:168
msgid "Download Template CSV"
msgstr ""
msgstr "Descargar Plantilla CSV"
#: apps/web/src/app/(dashboard)/templates/[id]/template-page-view-documents-table.tsx:208
#: apps/web/src/components/formatter/document-status.tsx:34
@@ -2659,7 +2659,7 @@ msgstr "Falló al actualizar el webhook"
#: packages/email/templates/bulk-send-complete.tsx:55
msgid "Failed: {failedCount}"
msgstr ""
msgstr "Fallidos: {failedCount}"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:100
msgid "Field character limit"
@@ -2721,7 +2721,7 @@ msgstr "Si tiene alguna pregunta sobre esta divulgación, firmas electrónicas o
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:147
msgid "For each recipient, provide their email (required) and name (optional) in separate columns. Download the template CSV below for the correct format."
msgstr ""
msgstr "Para cada destinatario, proporciona su correo electrónico (obligatorio) y nombre (opcional) en columnas separadas. Descarga el modelo CSV a continuación para el formato correcto."
#: packages/lib/server-only/auth/send-forgot-password.ts:61
msgid "Forgot Password?"
@@ -2834,7 +2834,7 @@ msgstr "Hola, soy Timur"
#: packages/email/templates/bulk-send-complete.tsx:36
msgid "Hi {userName},"
msgstr ""
msgstr "Hola, {userName},"
#: packages/email/templates/reset-password.tsx:56
msgid "Hi, {userName} <0>({userEmail})</0>"
@@ -3238,7 +3238,7 @@ msgstr "Máx"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:227
msgid "Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use template defaults."
msgstr ""
msgstr "Tamaño máximo de archivo: 4MB. Máximo 100 filas por carga. Los valores en blanco usarán los valores predeterminados de la plantilla."
#: packages/lib/constants/teams.ts:12
msgid "Member"
@@ -3868,7 +3868,7 @@ msgstr "Por favor, escribe <0>{0}</0> para confirmar."
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:172
msgid "Pre-formatted CSV template with example data."
msgstr ""
msgstr "Plantilla CSV preformateada con datos de ejemplo."
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:214
#: apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx:58
@@ -4392,7 +4392,7 @@ msgstr "Enviar documentos en nombre del equipo usando la dirección de correo el
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:253
msgid "Send documents to recipients immediately"
msgstr ""
msgstr "Enviar documentos a los destinatarios inmediatamente"
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:200
msgid "Send on Behalf of Team"
@@ -4864,11 +4864,11 @@ msgstr "Clave de acceso creada con éxito"
#: packages/email/templates/bulk-send-complete.tsx:52
msgid "Successfully created: {successCount}"
msgstr ""
msgstr "Creado con éxito: {successCount}"
#: packages/email/templates/bulk-send-complete.tsx:44
msgid "Summary:"
msgstr ""
msgstr "Resumen:"
#: apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx:57
msgid "System Requirements"
@@ -5089,7 +5089,7 @@ msgstr "Texto"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx:140
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:124
msgid "Text Align"
msgstr ""
msgstr "Alineación de texto"
#: apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx:157
msgid "Text Color"
@@ -5177,7 +5177,7 @@ msgstr "Los eventos que activarán un webhook para ser enviado a tu URL."
#: packages/email/templates/bulk-send-complete.tsx:62
msgid "The following errors occurred:"
msgstr ""
msgstr "Se produjeron los siguientes errores:"
#: packages/email/templates/team-delete.tsx:37
msgid "The following team has been deleted by its owner. You will no longer be able to access this team and its documents"
@@ -5637,7 +5637,7 @@ msgstr "Total de destinatarios"
#: packages/email/templates/bulk-send-complete.tsx:49
msgid "Total rows processed: {totalProcessed}"
msgstr ""
msgstr "Filas totales procesadas: {totalProcessed}"
#: apps/web/src/app/(dashboard)/admin/stats/page.tsx:150
msgid "Total Signers that Signed Up"
@@ -5899,7 +5899,7 @@ msgstr "Actualizar"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:132
msgid "Upload a CSV file to create multiple documents from this template. Each row represents one document with its recipient details."
msgstr ""
msgstr "Sube un archivo CSV para crear múltiples documentos a partir de esta plantilla. Cada fila representa un documento con los detalles del destinatario."
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:426
msgid "Upload a custom document to use instead of the template's default document"
@@ -5907,7 +5907,7 @@ msgstr "Sube un documento personalizado para usar en lugar del documento predete
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:267
msgid "Upload and Process"
msgstr ""
msgstr "Subir y procesar"
#: apps/web/src/components/forms/avatar-image.tsx:179
msgid "Upload Avatar"
@@ -5915,7 +5915,7 @@ msgstr "Subir avatar"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:198
msgid "Upload CSV"
msgstr ""
msgstr "Subir CSV"
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:419
msgid "Upload custom document"
@@ -6594,7 +6594,7 @@ msgstr "Puedes ver documentos asociados a este correo electrónico y usar esta i
#: packages/email/templates/bulk-send-complete.tsx:76
msgid "You can view the created documents in your dashboard under the \"Documents created from template\" section."
msgstr ""
msgstr "Puedes ver los documentos creados en tu panel de control bajo la sección \"Documentos creados a partir de la plantilla\"."
#: packages/email/template-components/template-document-rejected.tsx:37
msgid "You can view the document and its status by clicking the button below."
@@ -6807,11 +6807,11 @@ msgstr "Tus preferencias de marca han sido actualizadas"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:97
msgid "Your bulk send has been initiated. You will receive an email notification upon completion."
msgstr ""
msgstr "Tu envío masivo ha sido iniciado. Recibirás una notificación por correo electrónico al completarse."
#: packages/email/templates/bulk-send-complete.tsx:40
msgid "Your bulk send operation for template \"{templateName}\" has completed."
msgstr ""
msgstr "Tu operación de envío masivo para la plantilla \"{templateName}\" ha sido completada."
#: apps/web/src/app/(dashboard)/settings/billing/page.tsx:125
msgid "Your current plan is past due. Please update your payment information."
@@ -6962,3 +6962,4 @@ msgstr "¡Tu token se creó con éxito! ¡Asegúrate de copiarlo porque no podr
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx:86
msgid "Your tokens will be shown here once you create them."
msgstr "Tus tokens se mostrarán aquí una vez que los crees."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: fr\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-01-30 03:57\n"
"PO-Revision-Date: 2025-01-30 06:04\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
@@ -163,7 +163,7 @@ msgstr "{inviterName} vous a retiré du document<0/>\"{documentName}\""
#: packages/email/template-components/template-document-invite.tsx:61
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {0}<0/>\"{documentName}\""
msgstr ""
msgstr "{inviterName} représentant \"{teamName}\" vous a invité à {0}<0/>\"{documentName}\""
#: packages/email/templates/document-invite.tsx:45
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {action} {documentName}"
@@ -308,7 +308,7 @@ msgstr "{signerName} a rejeté le document \"{documentName}\"."
#: packages/email/template-components/template-document-invite.tsx:68
msgid "{teamName} has invited you to {0}<0/>\"{documentName}\""
msgstr ""
msgstr "{teamName} vous a invité à {0}<0/>\"{documentName}\""
#: packages/email/templates/document-invite.tsx:46
msgid "{teamName} has invited you to {action} {documentName}"
@@ -360,7 +360,7 @@ msgstr "<0>{teamName}</0> a demandé à utiliser votre adresse e-mail pour leur
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:463
msgid "<0>Click to upload</0> or drag and drop"
msgstr "<0>Cliquez pour télécharger</0> ou faites glisser et déposez"
msgstr "<0>Cliquez pour importer</0> ou faites glisser et déposez"
#: packages/ui/primitives/template-flow/add-template-settings.tsx:287
msgid "<0>Email</0> - The recipient will be emailed the document to sign, approve, etc."
@@ -1009,7 +1009,7 @@ msgstr "Une erreur est survenue lors de la mise à jour de votre profil."
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:108
msgid "An error occurred while uploading your document."
msgstr "Une erreur est survenue lors du téléchargement de votre document."
msgstr "Une erreur est survenue lors de l'importation de votre document."
#: apps/web/src/app/(dashboard)/admin/documents/[id]/super-delete-document-dialog.tsx:58
#: apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx:81
@@ -1232,20 +1232,20 @@ msgstr "Importation en masse"
#: packages/lib/jobs/definitions/internal/bulk-send-template.handler.ts:203
msgid "Bulk Send Complete: {0}"
msgstr ""
msgstr "Envoi en masse terminé : {0}"
#: packages/email/templates/bulk-send-complete.tsx:30
msgid "Bulk send operation complete for template \"{templateName}\""
msgstr ""
msgstr "Envoi groupé terminé pour le modèle \"{templateName}\""
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:128
msgid "Bulk Send Template via CSV"
msgstr ""
msgstr "Envoi de modèle groupé via CSV"
#: apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx:97
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:120
msgid "Bulk Send via CSV"
msgstr ""
msgstr "Envoi en masse via CSV"
#: packages/email/templates/team-invite.tsx:84
msgid "by <0>{senderName}</0>"
@@ -1430,7 +1430,7 @@ msgstr "Cliquez ici pour réessayer"
#: apps/web/src/components/(teams)/dialogs/invite-team-member-dialog.tsx:392
msgid "Click here to upload"
msgstr "Cliquez ici pour télécharger"
msgstr "Cliquez ici pour importer"
#: apps/web/src/components/(dashboard)/avatar/avatar-with-recipient.tsx:52
#: apps/web/src/components/(dashboard)/avatar/avatar-with-recipient.tsx:65
@@ -1597,11 +1597,11 @@ msgstr "Continuer vers la connexion"
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:185
msgid "Controls the default language of an uploaded document. This will be used as the language in email communications with the recipients."
msgstr "Contrôle la langue par défaut d'un document téléchargé. Cela sera utilisé comme langue dans les communications par e-mail avec les destinataires."
msgstr "Contrôle la langue par défaut d'un document importé. Cela sera utilisé comme langue dans les communications par e-mail avec les destinataires."
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:153
msgid "Controls the default visibility of an uploaded document."
msgstr "Contrôle la visibilité par défaut d'un document téléchargé."
msgstr "Contrôle la visibilité par défaut d'un document importé."
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:232
msgid "Controls the formatting of the message that will be sent when inviting a recipient to sign a document. If a custom message has been provided while configuring the document, it will be used instead."
@@ -1788,7 +1788,7 @@ msgstr "Créé le {0}"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:143
msgid "CSV Structure"
msgstr ""
msgstr "Structure CSV"
#: apps/web/src/components/forms/password.tsx:112
msgid "Current Password"
@@ -1800,7 +1800,7 @@ msgstr "Le mot de passe actuel est incorrect."
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:154
msgid "Current recipients:"
msgstr ""
msgstr "Destinataires actuels :"
#: apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx:28
msgid "Daily"
@@ -2255,11 +2255,11 @@ msgstr "Document mis à jour"
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:55
msgid "Document upload disabled due to unpaid invoices"
msgstr "Téléchargement du document désactivé en raison de factures impayées"
msgstr "Importation de documents désactivé en raison de factures impayées"
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:85
msgid "Document uploaded"
msgstr "Document téléchargé"
msgstr "Document importé"
#: apps/web/src/app/(signing)/sign/[token]/complete/page.tsx:131
msgid "Document Viewed"
@@ -2328,7 +2328,7 @@ msgstr "Télécharger le certificat"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:168
msgid "Download Template CSV"
msgstr ""
msgstr "Télécharger le modèle CSV"
#: apps/web/src/app/(dashboard)/templates/[id]/template-page-view-documents-table.tsx:208
#: apps/web/src/components/formatter/document-status.tsx:34
@@ -2659,7 +2659,7 @@ msgstr "Échec de la mise à jour du webhook"
#: packages/email/templates/bulk-send-complete.tsx:55
msgid "Failed: {failedCount}"
msgstr ""
msgstr "Échoués : {failedCount}"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:100
msgid "Field character limit"
@@ -2721,7 +2721,7 @@ msgstr "Pour toute question concernant cette divulgation, les signatures électr
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:147
msgid "For each recipient, provide their email (required) and name (optional) in separate columns. Download the template CSV below for the correct format."
msgstr ""
msgstr "Pour chaque destinataire, fournissez son e-mail (obligatoire) et son nom (facultatif) dans des colonnes séparées. Téléchargez le modèle CSV ci-dessous pour obtenir le format requis."
#: packages/lib/server-only/auth/send-forgot-password.ts:61
msgid "Forgot Password?"
@@ -2770,7 +2770,7 @@ msgstr "Authentification d'action de destinataire globale"
#: apps/web/src/components/partials/not-found.tsx:67
#: packages/ui/primitives/document-flow/document-flow-root.tsx:142
msgid "Go Back"
msgstr "Retourner"
msgstr "Retour"
#: apps/web/src/app/(unauthenticated)/verify-email/[token]/client.tsx:48
#: apps/web/src/app/(unauthenticated)/verify-email/[token]/page.tsx:73
@@ -2834,7 +2834,7 @@ msgstr "Salut, je suis Timur"
#: packages/email/templates/bulk-send-complete.tsx:36
msgid "Hi {userName},"
msgstr ""
msgstr "Bonjour {userName},"
#: packages/email/templates/reset-password.tsx:56
msgid "Hi, {userName} <0>({userEmail})</0>"
@@ -2856,7 +2856,7 @@ msgstr "Je suis un signataire de ce document"
#: packages/lib/constants/recipient-roles.ts:47
msgid "I am a viewer of this document"
msgstr "Je suis un visualiseur de ce document"
msgstr "Je suis un lecteur de ce document"
#: packages/lib/constants/recipient-roles.ts:45
msgid "I am an approver of this document"
@@ -3234,11 +3234,11 @@ msgstr "MAU (document terminé)"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx:209
msgid "Max"
msgstr ""
msgstr "Maximum"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:227
msgid "Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use template defaults."
msgstr ""
msgstr "Taille maximale du fichier : 4 Mo. Maximum de 100 lignes par importation. Les valeurs vides utiliseront les valeurs par défaut du modèle."
#: packages/lib/constants/teams.ts:12
msgid "Member"
@@ -3868,7 +3868,7 @@ msgstr "Veuillez taper <0>{0}</0> pour confirmer."
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:172
msgid "Pre-formatted CSV template with example data."
msgstr ""
msgstr "Modèle CSV pré-formaté avec des données d'exemple."
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:214
#: apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx:58
@@ -4392,7 +4392,7 @@ msgstr "Envoyer des documents au nom de l'équipe en utilisant l'adresse e-mail"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:253
msgid "Send documents to recipients immediately"
msgstr ""
msgstr "Envoyer les documents aux destinataires immédiatement"
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:200
msgid "Send on Behalf of Team"
@@ -4864,11 +4864,11 @@ msgstr "Clé d'authentification créée avec succès"
#: packages/email/templates/bulk-send-complete.tsx:52
msgid "Successfully created: {successCount}"
msgstr ""
msgstr "Créés avec succès : {successCount}"
#: packages/email/templates/bulk-send-complete.tsx:44
msgid "Summary:"
msgstr ""
msgstr "Résumé :"
#: apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx:57
msgid "System Requirements"
@@ -5032,7 +5032,7 @@ msgstr "Modèle supprimé"
#: apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx:66
msgid "Template document uploaded"
msgstr "Document modèle téléchargé"
msgstr "Document modèle importé"
#: apps/web/src/app/(dashboard)/templates/duplicate-template-dialog.tsx:41
msgid "Template duplicated"
@@ -5089,7 +5089,7 @@ msgstr "Texte"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx:140
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:124
msgid "Text Align"
msgstr ""
msgstr "Alignement du texte"
#: apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx:157
msgid "Text Color"
@@ -5097,11 +5097,11 @@ msgstr "Couleur du texte"
#: apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx:24
msgid "Thank you for using Documenso to perform your electronic document signing. The purpose of this disclosure is to inform you about the process, legality, and your rights regarding the use of electronic signatures on our platform. By opting to use an electronic signature, you are agreeing to the terms and conditions outlined below."
msgstr "Merci d'utiliser Documenso pour signer vos documents électroniquement. L'objectif de cette divulgation est de vous informer sur le processus, la légalité et vos droits concernant l'utilisation des signatures électroniques sur notre plateforme. En choisissant d'utiliser une signature électronique, vous acceptez les termes et conditions énoncés ci-dessous."
msgstr "Merci d'utiliser Documenso pour signer vos documents électroniquement. L'objectif de cette clause est de vous informer sur le processus, la légalité et vos droits concernant l'utilisation de la signature électronique sur notre plateforme. En choisissant d'utiliser un sytème de signature électronique, vous acceptez les termes et conditions exposés ci-dessous."
#: packages/email/template-components/template-forgot-password.tsx:25
msgid "That's okay, it happens! Click the button below to reset your password."
msgstr "C'est d'accord, cela arrive ! Cliquez sur le bouton ci-dessous pour réinitialiser votre mot de passe."
msgstr "Ce n'est pas grave, cela arrive ! Cliquez sur le bouton ci-dessous pour réinitialiser votre mot de passe."
#: apps/web/src/app/(dashboard)/admin/users/[id]/delete-user-dialog.tsx:52
msgid "The account has been deleted successfully."
@@ -5177,7 +5177,7 @@ msgstr "Les événements qui déclencheront un webhook à envoyer à votre URL."
#: packages/email/templates/bulk-send-complete.tsx:62
msgid "The following errors occurred:"
msgstr ""
msgstr "Les erreurs suivantes se sont produites :"
#: packages/email/templates/team-delete.tsx:37
msgid "The following team has been deleted by its owner. You will no longer be able to access this team and its documents"
@@ -5331,7 +5331,7 @@ msgstr "Le webhook a été créé avec succès."
#: apps/web/src/app/(dashboard)/documents/empty-state.tsx:25
msgid "There are no active drafts at the current moment. You can upload a document to start drafting."
msgstr "Il n'y a pas de brouillons actifs pour le moment. Vous pouvez télécharger un document pour commencer à rédiger."
msgstr "Il n'y a pas de brouillons actifs pour le moment. Vous pouvez importer un document pour commencer un brouillon."
#: apps/web/src/app/(dashboard)/documents/empty-state.tsx:20
msgid "There are no completed documents yet. Documents that you have created or received will appear here once completed."
@@ -5637,7 +5637,7 @@ msgstr "Total des destinataires"
#: packages/email/templates/bulk-send-complete.tsx:49
msgid "Total rows processed: {totalProcessed}"
msgstr ""
msgstr "Lignes totales traitées : {totalProcessed}"
#: apps/web/src/app/(dashboard)/admin/stats/page.tsx:150
msgid "Total Signers that Signed Up"
@@ -5899,27 +5899,27 @@ msgstr "Améliorer"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:132
msgid "Upload a CSV file to create multiple documents from this template. Each row represents one document with its recipient details."
msgstr ""
msgstr "Importer un fichier CSV pour créer plusieurs documents à partir de ce modèle. Chaque ligne représente un document avec les coordonnées de son destinataire."
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:426
msgid "Upload a custom document to use instead of the template's default document"
msgstr "Téléchargez un document personnalisé à utiliser à la place du document par défaut du modèle"
msgstr "Importer un document personnalisé à utiliser à la place du modèle par défaut"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:267
msgid "Upload and Process"
msgstr ""
msgstr "Importer et traiter"
#: apps/web/src/components/forms/avatar-image.tsx:179
msgid "Upload Avatar"
msgstr "Télécharger un avatar"
msgstr "Importer un avatar"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:198
msgid "Upload CSV"
msgstr ""
msgstr "Importer le CSV"
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:419
msgid "Upload custom document"
msgstr "Télécharger un document personnalisé"
msgstr "Importer un document personnalisé"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:529
msgid "Upload Signature"
@@ -5927,7 +5927,7 @@ msgstr "Importer une signature"
#: packages/ui/primitives/document-dropzone.tsx:70
msgid "Upload Template Document"
msgstr "Télécharger le document modèle"
msgstr "Importer le document modèle"
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/branding-preferences.tsx:256
msgid "Upload your brand logo (max 5MB, JPG, PNG, or WebP)"
@@ -5940,15 +5940,15 @@ msgstr "Téléversé par"
#: apps/web/src/components/forms/avatar-image.tsx:91
msgid "Uploaded file is too large"
msgstr "Le fichier téléchargé est trop volumineux"
msgstr "Le fichier importé est trop volumineux"
#: apps/web/src/components/forms/avatar-image.tsx:92
msgid "Uploaded file is too small"
msgstr "Le fichier téléchargé est trop petit"
msgstr "Le fichier importé est trop petit"
#: apps/web/src/components/forms/avatar-image.tsx:93
msgid "Uploaded file not an allowed file type"
msgstr "Le fichier téléchargé n'est pas un type de fichier autorisé"
msgstr "Le fichier importé n'est pas un type de fichier autorisé"
#: apps/web/src/app/(dashboard)/templates/[id]/template-page-view.tsx:175
msgid "Use"
@@ -6040,7 +6040,7 @@ msgstr "Vérifiez votre adresse e-mail pour débloquer toutes les fonctionnalit
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:60
msgid "Verify your email to upload documents."
msgstr "Vérifiez votre e-mail pour télécharger des documents."
msgstr "Vérifiez votre e-mail pour importer des documents."
#: packages/email/templates/confirm-team-email.tsx:71
msgid "Verify your team email address"
@@ -6137,11 +6137,11 @@ msgstr "Vu"
#: packages/lib/constants/recipient-roles.ts:32
msgid "Viewer"
msgstr "Visiteur"
msgstr "Lecteur"
#: packages/lib/constants/recipient-roles.ts:33
msgid "Viewers"
msgstr "Spectateurs"
msgstr "Lecteurs"
#: packages/lib/constants/recipient-roles.ts:31
msgid "Viewing"
@@ -6526,7 +6526,7 @@ msgstr "Vous êtes sur le point d'envoyer ce document aux destinataires. Êtes-v
#: apps/web/src/app/(dashboard)/settings/billing/page.tsx:80
msgid "You are currently on the <0>Free Plan</0>."
msgstr "Vous êtes actuellement sur le <0>Plan Gratuit</0>."
msgstr "Vous êtes actuellement sur l'<0>Abonnement Gratuit</0>."
#: apps/web/src/components/(teams)/dialogs/update-team-member-dialog.tsx:148
msgid "You are currently updating <0>{teamMemberName}.</0>"
@@ -6594,7 +6594,7 @@ msgstr "Vous pouvez voir les documents associés à cet e-mail et utiliser cette
#: packages/email/templates/bulk-send-complete.tsx:76
msgid "You can view the created documents in your dashboard under the \"Documents created from template\" section."
msgstr ""
msgstr "Vous pouvez voir les documents créés dans votre tableau de bord sous la section \"Documents créés à partir du modèle\"."
#: packages/email/template-components/template-document-rejected.tsx:37
msgid "You can view the document and its status by clicking the button below."
@@ -6610,11 +6610,11 @@ msgstr "Vous ne pouvez pas modifier un membre de l'équipe qui a un rôle plus
#: packages/ui/primitives/document-dropzone.tsx:43
msgid "You cannot upload documents at this time."
msgstr "Vous ne pouvez pas télécharger de documents pour le moment."
msgstr "Vous ne pouvez pas importer de documents pour le moment."
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:103
msgid "You cannot upload encrypted PDFs"
msgstr "Vous ne pouvez pas télécharger de PDF cryptés"
msgstr "Vous ne pouvez pas importer de PDF cryptés"
#: apps/web/src/app/(dashboard)/settings/billing/billing-portal-button.tsx:46
msgid "You do not currently have a customer record, this should not happen. Please contact support for assistance."
@@ -6678,11 +6678,11 @@ msgstr "Vous n'avez pas encore de webhooks. Vos webhooks seront affichés ici un
#: apps/web/src/app/(dashboard)/templates/empty-state.tsx:15
msgid "You have not yet created any templates. To create a template please upload one."
msgstr "Vous n'avez pas encore créé de modèles. Pour créer un modèle, veuillez en télécharger un."
msgstr "Vous n'avez pas encore créé de modèles. Pour créer un modèle, veuillez en importer un."
#: apps/web/src/app/(dashboard)/documents/empty-state.tsx:30
msgid "You have not yet created or received any documents. To create a document please upload one."
msgstr "Vous n'avez pas encore créé ou reçu de documents. Pour créer un document, veuillez en télécharger un."
msgstr "Vous n'avez pas encore créé ou reçu de documents. Pour créer un document, veuillez en importer un."
#: apps/web/src/app/(dashboard)/templates/template-direct-link-dialog.tsx:229
msgid "You have reached the maximum limit of {0} direct templates. <0>Upgrade your account to continue!</0>"
@@ -6690,7 +6690,7 @@ msgstr "Vous avez atteint la limite maximale de {0} modèles directs. <0>Mettez
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:106
msgid "You have reached your document limit for this month. Please upgrade your plan."
msgstr "Vous avez atteint votre limite de documents pour ce mois. Veuillez passer à un plan supérieur."
msgstr "Vous avez atteint votre limite de documents pour ce mois. Veuillez passer à l'abonnement supérieur."
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:56
#: packages/ui/primitives/document-dropzone.tsx:69
@@ -6807,15 +6807,15 @@ msgstr "Vos préférences de branding ont été mises à jour"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:97
msgid "Your bulk send has been initiated. You will receive an email notification upon completion."
msgstr ""
msgstr "Votre envoi groupé a été initié. Vous recevrez une notification par email une fois terminé."
#: packages/email/templates/bulk-send-complete.tsx:40
msgid "Your bulk send operation for template \"{templateName}\" has completed."
msgstr ""
msgstr "Votre envoi groupé pour le modèle \"{templateName}\" est terminé."
#: apps/web/src/app/(dashboard)/settings/billing/page.tsx:125
msgid "Your current plan is past due. Please update your payment information."
msgstr "Votre plan actuel est en retard. Veuillez mettre à jour vos informations de paiement."
msgstr "Votre abonnement actuel est arrivé à échéance. Veuillez mettre à jour vos informations de paiement."
#: apps/web/src/components/templates/manage-public-template-dialog.tsx:249
msgid "Your direct signing templates"
@@ -6823,7 +6823,7 @@ msgstr "Vos modèles de signature directe"
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:123
msgid "Your document failed to upload."
msgstr "Votre document a échoué à se télécharger."
msgstr "L'importation de votre document a échoué."
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:169
msgid "Your document has been created from the template successfully."
@@ -6847,11 +6847,11 @@ msgstr "Votre document a été dupliqué avec succès."
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:86
msgid "Your document has been uploaded successfully."
msgstr "Votre document a été téléchargé avec succès."
msgstr "Votre document a été importé avec succès."
#: apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx:68
msgid "Your document has been uploaded successfully. You will be redirected to the template page."
msgstr "Votre document a été téléchargé avec succès. Vous serez redirigé vers la page de modèle."
msgstr "Votre document a été importé avec succès. Vous serez redirigé vers la page de modèle."
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:104
msgid "Your document preferences have been updated"
@@ -6962,3 +6962,4 @@ msgstr "Votre token a été créé avec succès ! Assurez-vous de le copier car
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx:86
msgid "Your tokens will be shown here once you create them."
msgstr "Vos tokens seront affichés ici une fois que vous les aurez créés."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: it\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-01-30 03:57\n"
"PO-Revision-Date: 2025-01-30 06:04\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -163,7 +163,7 @@ msgstr "{inviterName} ti ha rimosso dal documento<0/>\"{documentName}\""
#: packages/email/template-components/template-document-invite.tsx:61
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {0}<0/>\"{documentName}\""
msgstr ""
msgstr "{inviterName} per conto di \"{teamName}\" ti ha invitato a {0}<0/>\"{documentName}\""
#: packages/email/templates/document-invite.tsx:45
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {action} {documentName}"
@@ -308,7 +308,7 @@ msgstr "{signerName} ha rifiutato il documento \"{documentName}\"."
#: packages/email/template-components/template-document-invite.tsx:68
msgid "{teamName} has invited you to {0}<0/>\"{documentName}\""
msgstr ""
msgstr "{teamName} ti ha invitato a {0}<0/>\"{documentName}\""
#: packages/email/templates/document-invite.tsx:46
msgid "{teamName} has invited you to {action} {documentName}"
@@ -1232,20 +1232,20 @@ msgstr "Importazione Massiva"
#: packages/lib/jobs/definitions/internal/bulk-send-template.handler.ts:203
msgid "Bulk Send Complete: {0}"
msgstr ""
msgstr "Invio Massivo Completato: {0}"
#: packages/email/templates/bulk-send-complete.tsx:30
msgid "Bulk send operation complete for template \"{templateName}\""
msgstr ""
msgstr "Operazione di invio massivo completata per il modello \"{templateName}\""
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:128
msgid "Bulk Send Template via CSV"
msgstr ""
msgstr "Invio modello in blocco tramite CSV"
#: apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx:97
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:120
msgid "Bulk Send via CSV"
msgstr ""
msgstr "Invio Massivo via CSV"
#: packages/email/templates/team-invite.tsx:84
msgid "by <0>{senderName}</0>"
@@ -1788,7 +1788,7 @@ msgstr "Creato il {0}"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:143
msgid "CSV Structure"
msgstr ""
msgstr "Struttura CSV"
#: apps/web/src/components/forms/password.tsx:112
msgid "Current Password"
@@ -1800,7 +1800,7 @@ msgstr "La password corrente è errata."
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:154
msgid "Current recipients:"
msgstr ""
msgstr "Destinatari attuali:"
#: apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx:28
msgid "Daily"
@@ -2328,7 +2328,7 @@ msgstr "Scarica il certificato"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:168
msgid "Download Template CSV"
msgstr ""
msgstr "Scarica Modello CSV"
#: apps/web/src/app/(dashboard)/templates/[id]/template-page-view-documents-table.tsx:208
#: apps/web/src/components/formatter/document-status.tsx:34
@@ -2659,7 +2659,7 @@ msgstr "Aggiornamento webhook fallito"
#: packages/email/templates/bulk-send-complete.tsx:55
msgid "Failed: {failedCount}"
msgstr ""
msgstr "Falliti: {failedCount}"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:100
msgid "Field character limit"
@@ -2721,7 +2721,7 @@ msgstr "Per qualsiasi domanda riguardante questa divulgazione, le firme elettron
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:147
msgid "For each recipient, provide their email (required) and name (optional) in separate columns. Download the template CSV below for the correct format."
msgstr ""
msgstr "Per ogni destinatario, fornisci la loro email (obbligatoria) e il nome (opzionale) in colonne separate. Scarica il modello CSV qui sotto per il formato corretto."
#: packages/lib/server-only/auth/send-forgot-password.ts:61
msgid "Forgot Password?"
@@ -2834,7 +2834,7 @@ msgstr "Ciao, sono Timur"
#: packages/email/templates/bulk-send-complete.tsx:36
msgid "Hi {userName},"
msgstr ""
msgstr "Ciao {userName},"
#: packages/email/templates/reset-password.tsx:56
msgid "Hi, {userName} <0>({userEmail})</0>"
@@ -3238,7 +3238,7 @@ msgstr ""
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:227
msgid "Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use template defaults."
msgstr ""
msgstr "Dimensione massima del file: 4MB. Massimo 100 righe per caricamento. I valori vuoti utilizzeranno i valori predefiniti del modello."
#: packages/lib/constants/teams.ts:12
msgid "Member"
@@ -3868,7 +3868,7 @@ msgstr "Si prega di digitare <0>{0}</0> per confermare."
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:172
msgid "Pre-formatted CSV template with example data."
msgstr ""
msgstr "Modello CSV preformattato con dati di esempio."
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:214
#: apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx:58
@@ -4392,7 +4392,7 @@ msgstr "Invia documenti a nome del team utilizzando l'indirizzo email"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:253
msgid "Send documents to recipients immediately"
msgstr ""
msgstr "Invia documenti ai destinatari immediatamente"
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:200
msgid "Send on Behalf of Team"
@@ -4864,11 +4864,11 @@ msgstr "Chiave di accesso creata con successo"
#: packages/email/templates/bulk-send-complete.tsx:52
msgid "Successfully created: {successCount}"
msgstr ""
msgstr "Creati con successo: {successCount}"
#: packages/email/templates/bulk-send-complete.tsx:44
msgid "Summary:"
msgstr ""
msgstr "Sommario:"
#: apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx:57
msgid "System Requirements"
@@ -5089,7 +5089,7 @@ msgstr "Testo"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx:140
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:124
msgid "Text Align"
msgstr ""
msgstr "Allineamento del testo"
#: apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx:157
msgid "Text Color"
@@ -5177,7 +5177,7 @@ msgstr "Gli eventi che scateneranno un webhook da inviare al tuo URL."
#: packages/email/templates/bulk-send-complete.tsx:62
msgid "The following errors occurred:"
msgstr ""
msgstr "Si sono verificati i seguenti errori:"
#: packages/email/templates/team-delete.tsx:37
msgid "The following team has been deleted by its owner. You will no longer be able to access this team and its documents"
@@ -5637,7 +5637,7 @@ msgstr "Totale destinatari"
#: packages/email/templates/bulk-send-complete.tsx:49
msgid "Total rows processed: {totalProcessed}"
msgstr ""
msgstr "Righe totali elaborate: {totalProcessed}"
#: apps/web/src/app/(dashboard)/admin/stats/page.tsx:150
msgid "Total Signers that Signed Up"
@@ -5899,7 +5899,7 @@ msgstr "Aggiorna"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:132
msgid "Upload a CSV file to create multiple documents from this template. Each row represents one document with its recipient details."
msgstr ""
msgstr "Carica un file CSV per creare più documenti da questo modello. Ogni riga rappresenta un documento con i dettagli del destinatario."
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:426
msgid "Upload a custom document to use instead of the template's default document"
@@ -5907,7 +5907,7 @@ msgstr "Carica un documento personalizzato da utilizzare al posto del documento
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:267
msgid "Upload and Process"
msgstr ""
msgstr "Carica e elabora"
#: apps/web/src/components/forms/avatar-image.tsx:179
msgid "Upload Avatar"
@@ -5915,7 +5915,7 @@ msgstr "Carica Avatar"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:198
msgid "Upload CSV"
msgstr ""
msgstr "Carica CSV"
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:419
msgid "Upload custom document"
@@ -6594,7 +6594,7 @@ msgstr "Puoi visualizzare i documenti associati a questa email e utilizzare ques
#: packages/email/templates/bulk-send-complete.tsx:76
msgid "You can view the created documents in your dashboard under the \"Documents created from template\" section."
msgstr ""
msgstr "Puoi visualizzare i documenti creati nel tuo dashboard nella sezione \"Documenti creati dal modello\"."
#: packages/email/template-components/template-document-rejected.tsx:37
msgid "You can view the document and its status by clicking the button below."
@@ -6807,11 +6807,11 @@ msgstr "Le tue preferenze di branding sono state aggiornate"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:97
msgid "Your bulk send has been initiated. You will receive an email notification upon completion."
msgstr ""
msgstr "Il tuo invio massivo è stato avviato. Riceverai una notifica via email al completamento."
#: packages/email/templates/bulk-send-complete.tsx:40
msgid "Your bulk send operation for template \"{templateName}\" has completed."
msgstr ""
msgstr "La tua operazione di invio massivo per il modello \"{templateName}\" è stata completata."
#: apps/web/src/app/(dashboard)/settings/billing/page.tsx:125
msgid "Your current plan is past due. Please update your payment information."
@@ -6962,3 +6962,4 @@ msgstr "Il tuo token è stato creato con successo! Assicurati di copiarlo perch
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx:86
msgid "Your tokens will be shown here once you create them."
msgstr "I tuoi token verranno mostrati qui una volta creati."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: pl\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-01-24 12:04\n"
"PO-Revision-Date: 2025-01-30 06:04\n"
"Last-Translator: \n"
"Language-Team: Polish\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
@@ -163,7 +163,7 @@ msgstr "{inviterName} usunął cię z dokumentu<0/>„{documentName}”"
#: packages/email/template-components/template-document-invite.tsx:61
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {0}<0/>\"{documentName}\""
msgstr ""
msgstr "{inviterName} w imieniu \"{teamName}\" zaprosił Cię do {0}<0/>\"{documentName}\""
#: packages/email/templates/document-invite.tsx:45
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {action} {documentName}"
@@ -308,7 +308,7 @@ msgstr "{signerName} odrzucił dokument \"{documentName}\"."
#: packages/email/template-components/template-document-invite.tsx:68
msgid "{teamName} has invited you to {0}<0/>\"{documentName}\""
msgstr ""
msgstr "{teamName} zaprosił Cię do {0}<0/>\"{documentName}\""
#: packages/email/templates/document-invite.tsx:46
msgid "{teamName} has invited you to {action} {documentName}"
@@ -1232,20 +1232,20 @@ msgstr "Import zbiorczy"
#: packages/lib/jobs/definitions/internal/bulk-send-template.handler.ts:203
msgid "Bulk Send Complete: {0}"
msgstr ""
msgstr "Zakończono wysyłkę zbiorczą: {0}"
#: packages/email/templates/bulk-send-complete.tsx:30
msgid "Bulk send operation complete for template \"{templateName}\""
msgstr ""
msgstr "Zakończono operację masowej wysyłki dla szablonu \"{templateName}\""
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:128
msgid "Bulk Send Template via CSV"
msgstr ""
msgstr "Szablon masowej wysyłki przez CSV"
#: apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx:97
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:120
msgid "Bulk Send via CSV"
msgstr ""
msgstr "Zbiorcza wysyłka przez CSV"
#: packages/email/templates/team-invite.tsx:84
msgid "by <0>{senderName}</0>"
@@ -1788,7 +1788,7 @@ msgstr "Utworzono {0}"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:143
msgid "CSV Structure"
msgstr ""
msgstr "Struktura CSV"
#: apps/web/src/components/forms/password.tsx:112
msgid "Current Password"
@@ -1800,7 +1800,7 @@ msgstr "Aktualne hasło jest niepoprawne."
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:154
msgid "Current recipients:"
msgstr ""
msgstr "Aktualni odbiorcy:"
#: apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx:28
msgid "Daily"
@@ -2328,7 +2328,7 @@ msgstr "Pobierz certyfikat"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:168
msgid "Download Template CSV"
msgstr ""
msgstr "Pobierz szablon CSV"
#: apps/web/src/app/(dashboard)/templates/[id]/template-page-view-documents-table.tsx:208
#: apps/web/src/components/formatter/document-status.tsx:34
@@ -2659,7 +2659,7 @@ msgstr "Nie udało się zaktualizować webhooku"
#: packages/email/templates/bulk-send-complete.tsx:55
msgid "Failed: {failedCount}"
msgstr ""
msgstr "Niepowodzenia: {failedCount}"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:100
msgid "Field character limit"
@@ -2721,7 +2721,7 @@ msgstr "W przypadku jakichkolwiek pytań dotyczących tego ujawnienia, podpisów
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:147
msgid "For each recipient, provide their email (required) and name (optional) in separate columns. Download the template CSV below for the correct format."
msgstr ""
msgstr "Dla każdego odbiorcy podaj jego email (wymagany) i nazwę (opcjonalnie) w oddzielnych kolumnach. Pobierz poniżej szablon CSV dla właściwego formatu."
#: packages/lib/server-only/auth/send-forgot-password.ts:61
msgid "Forgot Password?"
@@ -2834,7 +2834,7 @@ msgstr "Cześć, jestem Timur"
#: packages/email/templates/bulk-send-complete.tsx:36
msgid "Hi {userName},"
msgstr ""
msgstr "Cześć, {userName},"
#: packages/email/templates/reset-password.tsx:56
msgid "Hi, {userName} <0>({userEmail})</0>"
@@ -3238,7 +3238,7 @@ msgstr "Max"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:227
msgid "Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use template defaults."
msgstr ""
msgstr "Maksymalny rozmiar pliku: 4MB. Maksymalnie 100 wierszy na przesyłkę. Puste wartości zostaną zastąpione domyślnymi z szablonu."
#: packages/lib/constants/teams.ts:12
msgid "Member"
@@ -3868,7 +3868,7 @@ msgstr "Wpisz <0>{0}</0>, aby potwierdzić."
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:172
msgid "Pre-formatted CSV template with example data."
msgstr ""
msgstr "Wstępnie sformatowany szablon CSV z przykładowymi danymi."
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:214
#: apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx:58
@@ -4392,7 +4392,7 @@ msgstr "Wyślij dokumenty w imieniu zespołu, używając adresu e-mail"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:253
msgid "Send documents to recipients immediately"
msgstr ""
msgstr "Wyślij dokumenty do odbiorców natychmiast"
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:200
msgid "Send on Behalf of Team"
@@ -4864,11 +4864,11 @@ msgstr "Pomyślnie utworzono klucz uwierzytelniający"
#: packages/email/templates/bulk-send-complete.tsx:52
msgid "Successfully created: {successCount}"
msgstr ""
msgstr "Pomyślnie utworzono: {successCount}"
#: packages/email/templates/bulk-send-complete.tsx:44
msgid "Summary:"
msgstr ""
msgstr "Podsumowanie:"
#: apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx:57
msgid "System Requirements"
@@ -5089,7 +5089,7 @@ msgstr "Tekst"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx:140
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:124
msgid "Text Align"
msgstr ""
msgstr "Wyrównanie tekstu"
#: apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx:157
msgid "Text Color"
@@ -5177,7 +5177,7 @@ msgstr "Wydarzenia, które wyzwolą webhook do wysłania do Twojego URL."
#: packages/email/templates/bulk-send-complete.tsx:62
msgid "The following errors occurred:"
msgstr ""
msgstr "Wystąpiły następujące błędy:"
#: packages/email/templates/team-delete.tsx:37
msgid "The following team has been deleted by its owner. You will no longer be able to access this team and its documents"
@@ -5637,7 +5637,7 @@ msgstr "Łączna liczba odbiorców"
#: packages/email/templates/bulk-send-complete.tsx:49
msgid "Total rows processed: {totalProcessed}"
msgstr ""
msgstr "Łączna liczba przetworzonych wierszy: {totalProcessed}"
#: apps/web/src/app/(dashboard)/admin/stats/page.tsx:150
msgid "Total Signers that Signed Up"
@@ -5899,7 +5899,7 @@ msgstr "Ulepsz"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:132
msgid "Upload a CSV file to create multiple documents from this template. Each row represents one document with its recipient details."
msgstr ""
msgstr "Prześlij plik CSV, aby utworzyć wiele dokumentów z tego szablonu. Każda linia reprezentuje jeden dokument z jego szczegółami odbiorcy."
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:426
msgid "Upload a custom document to use instead of the template's default document"
@@ -5907,7 +5907,7 @@ msgstr "Prześlij niestandardowy dokument do użycia zamiast domyślnego dokumen
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:267
msgid "Upload and Process"
msgstr ""
msgstr "Prześlij i przetwórz"
#: apps/web/src/components/forms/avatar-image.tsx:179
msgid "Upload Avatar"
@@ -5915,7 +5915,7 @@ msgstr "Prześlij awatar"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:198
msgid "Upload CSV"
msgstr ""
msgstr "Prześlij CSV"
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:419
msgid "Upload custom document"
@@ -6594,7 +6594,7 @@ msgstr "Możesz wyświetlać dokumenty powiązane z tym e-mailem i używać tej
#: packages/email/templates/bulk-send-complete.tsx:76
msgid "You can view the created documents in your dashboard under the \"Documents created from template\" section."
msgstr ""
msgstr "Możesz zobaczyć utworzone dokumenty na swoim pulpicie w sekcji \"Dokumenty utworzone z szablonu\"."
#: packages/email/template-components/template-document-rejected.tsx:37
msgid "You can view the document and its status by clicking the button below."
@@ -6807,11 +6807,11 @@ msgstr "Preferencje dotyczące marki zostały zaktualizowane"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:97
msgid "Your bulk send has been initiated. You will receive an email notification upon completion."
msgstr ""
msgstr "Twoja masowa wysyłka została zainicjowana. Otrzymasz powiadomienie e-mail po jej zakończeniu."
#: packages/email/templates/bulk-send-complete.tsx:40
msgid "Your bulk send operation for template \"{templateName}\" has completed."
msgstr ""
msgstr "Twoja operacja masowej wysyłki dla szablonu \"{templateName}\" została zakończona."
#: apps/web/src/app/(dashboard)/settings/billing/page.tsx:125
msgid "Your current plan is past due. Please update your payment information."
@@ -6962,3 +6962,4 @@ msgstr "Twój token został pomyślnie utworzony! Upewnij się, że go skopiujes
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx:86
msgid "Your tokens will be shown here once you create them."
msgstr "Twoje tokeny będą tutaj wyświetlane po ich utworzeniu."

View File

@@ -28,6 +28,7 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
'DOCUMENT_DELETED', // When the document is soft deleted.
'DOCUMENT_FIELD_INSERTED', // When a field is inserted (signed/approved/etc) by a recipient.
'DOCUMENT_FIELD_UNINSERTED', // When a field is uninserted by a recipient.
'DOCUMENT_FIELD_PREFILLED', // When a field is prefilled by an assistant.
'DOCUMENT_VISIBILITY_UPDATED', // When the document visibility scope is updated
'DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED', // When the global access authentication is updated.
'DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED', // When the global action authentication is updated.
@@ -45,6 +46,7 @@ export const ZDocumentAuditLogEmailTypeSchema = z.enum([
'SIGNING_REQUEST',
'VIEW_REQUEST',
'APPROVE_REQUEST',
'ASSISTING_REQUEST',
'CC',
'DOCUMENT_COMPLETED',
]);
@@ -313,6 +315,83 @@ export const ZDocumentAuditLogEventDocumentFieldUninsertedSchema = z.object({
}),
});
/**
* Event: Document field prefilled by assistant.
*/
export const ZDocumentAuditLogEventDocumentFieldPrefilledSchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED),
data: ZBaseRecipientDataSchema.extend({
fieldId: z.string(),
// Organised into union to allow us to extend each field if required.
field: z.union([
z.object({
type: z.literal(FieldType.INITIALS),
data: z.string(),
}),
z.object({
type: z.literal(FieldType.EMAIL),
data: z.string(),
}),
z.object({
type: z.literal(FieldType.DATE),
data: z.string(),
}),
z.object({
type: z.literal(FieldType.NAME),
data: z.string(),
}),
z.object({
type: z.literal(FieldType.TEXT),
data: z.string(),
}),
z.object({
type: z.union([z.literal(FieldType.SIGNATURE), z.literal(FieldType.FREE_SIGNATURE)]),
data: z.string(),
}),
z.object({
type: z.literal(FieldType.RADIO),
data: z.string(),
}),
z.object({
type: z.literal(FieldType.CHECKBOX),
data: z.string(),
}),
z.object({
type: z.literal(FieldType.DROPDOWN),
data: z.string(),
}),
z.object({
type: z.literal(FieldType.NUMBER),
data: z.string(),
}),
]),
fieldSecurity: z.preprocess(
(input) => {
const legacyNoneSecurityType = JSON.stringify({
type: 'NONE',
});
// Replace legacy 'NONE' field security type with undefined.
if (
typeof input === 'object' &&
input !== null &&
JSON.stringify(input) === legacyNoneSecurityType
) {
return undefined;
}
return input;
},
z
.object({
type: ZRecipientActionAuthTypesSchema,
})
.optional(),
),
}),
});
export const ZDocumentAuditLogEventDocumentVisibilitySchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED),
data: ZGenericFromToSchema,
@@ -493,6 +572,7 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
ZDocumentAuditLogEventDocumentMovedToTeamSchema,
ZDocumentAuditLogEventDocumentFieldInsertedSchema,
ZDocumentAuditLogEventDocumentFieldUninsertedSchema,
ZDocumentAuditLogEventDocumentFieldPrefilledSchema,
ZDocumentAuditLogEventDocumentVisibilitySchema,
ZDocumentAuditLogEventDocumentGlobalAuthAccessUpdatedSchema,
ZDocumentAuditLogEventDocumentGlobalAuthActionUpdatedSchema,

View File

@@ -123,6 +123,42 @@ export const ZFieldMetaNotOptionalSchema = z.discriminatedUnion('type', [
export type TFieldMetaNotOptionalSchema = z.infer<typeof ZFieldMetaNotOptionalSchema>;
export const ZFieldMetaPrefillFieldsSchema = z
.object({
id: z.number(),
})
.and(
z.discriminatedUnion('type', [
z.object({
type: z.literal('text'),
label: z.string(),
value: z.string(),
}),
z.object({
type: z.literal('number'),
label: z.string(),
value: z.string(),
}),
z.object({
type: z.literal('radio'),
label: z.string(),
value: z.string(),
}),
z.object({
type: z.literal('checkbox'),
label: z.string(),
value: z.array(z.string()),
}),
z.object({
type: z.literal('dropdown'),
label: z.string(),
value: z.string(),
}),
]),
);
export type TFieldMetaPrefillFieldsSchema = z.infer<typeof ZFieldMetaPrefillFieldsSchema>;
export const ZFieldMetaSchema = z
.union([
// Handles an empty object being provided as fieldMeta.

View File

@@ -314,6 +314,10 @@ export const formatDocumentAuditLogAction = (
anonymous: msg`Field unsigned`,
identified: msg`${prefix} unsigned a field`,
}))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED }, () => ({
anonymous: msg`Field prefilled by assistant`,
identified: msg`${prefix} prefilled a field`,
}))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED }, () => ({
anonymous: msg`Document visibility updated`,
identified: msg`${prefix} updated the document visibility`,

View File

@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "RecipientRole" ADD VALUE 'ASSISTANT';

View File

@@ -1,29 +0,0 @@
-- CreateTable
CREATE TABLE "DocumentAccessToken" (
"id" TEXT NOT NULL,
"token" TEXT NOT NULL,
"documentId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"expiresAt" TIMESTAMP(3),
"revokedAt" TIMESTAMP(3),
"lastAccessedAt" TIMESTAMP(3),
"accessCount" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "DocumentAccessToken_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "DocumentAccessToken_token_key" ON "DocumentAccessToken"("token");
-- CreateIndex
CREATE UNIQUE INDEX "DocumentAccessToken_documentId_key" ON "DocumentAccessToken"("documentId");
-- CreateIndex
CREATE INDEX "DocumentAccessToken_token_idx" ON "DocumentAccessToken"("token");
-- CreateIndex
CREATE INDEX "DocumentAccessToken_documentId_idx" ON "DocumentAccessToken"("documentId");
-- AddForeignKey
ALTER TABLE "DocumentAccessToken" ADD CONSTRAINT "DocumentAccessToken_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,18 @@
/*
Warnings:
- You are about to drop the column `expires` on the `Session` table. All the data in the column will be lost.
- Added the required column `expiresAt` to the `Session` table without a default value. This is not possible if the table is not empty.
- Added the required column `updatedAt` to the `Session` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Account" ADD COLUMN "password" TEXT;
-- AlterTable
ALTER TABLE "Session" DROP COLUMN "expires",
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "expiresAt" TIMESTAMP(3) NOT NULL,
ADD COLUMN "ipAddress" TEXT,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL,
ADD COLUMN "userAgent" TEXT;

View File

@@ -270,18 +270,25 @@ model Account {
scope String?
id_token String? @db.Text
session_state String?
password String?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
id String @id @default(cuid())
sessionToken String @unique
userId Int
expires DateTime
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
ipAddress String?
userAgent String?
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
}
enum DocumentStatus {
@@ -329,8 +336,7 @@ model Document {
template Template? @relation(fields: [templateId], references: [id], onDelete: SetNull)
source DocumentSource
auditLogs DocumentAuditLog[]
documentAccessToken DocumentAccessToken?
auditLogs DocumentAuditLog[]
@@unique([documentDataId])
@@index([userId])
@@ -397,23 +403,6 @@ model DocumentMeta {
emailSettings Json? /// [DocumentEmailSettings] @zod.custom.use(ZDocumentEmailSettingsSchema)
}
model DocumentAccessToken {
id String @id @default(cuid())
token String @unique
documentId Int @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
expiresAt DateTime?
revokedAt DateTime?
lastAccessedAt DateTime?
accessCount Int @default(0)
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
@@index([token])
@@index([documentId])
}
enum ReadStatus {
NOT_OPENED
OPENED
@@ -435,6 +424,7 @@ enum RecipientRole {
SIGNER
VIEWER
APPROVER
ASSISTANT
}
/// @zod.import(["import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';"])

View File

@@ -5,6 +5,18 @@ import { hashSync } from '@documenso/lib/server-only/auth/hash';
import { prisma } from '..';
import { DocumentDataType, DocumentSource, Role, TeamMemberRole } from '../client';
import { seedPendingDocument } from './documents';
import { seedDirectTemplate, seedTemplate } from './templates';
const createDocumentData = async ({ documentData }: { documentData: string }) => {
return prisma.documentData.create({
data: {
type: DocumentDataType.BYTES_64,
data: documentData,
initialData: documentData,
},
});
};
export const seedDatabase = async () => {
const examplePdf = fs
@@ -39,35 +51,80 @@ export const seedDatabase = async () => {
update: {},
});
const examplePdfData = await prisma.documentData.upsert({
where: {
id: 'clmn0kv5k0000pe04vcqg5zla',
},
create: {
id: 'clmn0kv5k0000pe04vcqg5zla',
type: DocumentDataType.BYTES_64,
data: examplePdf,
initialData: examplePdf,
},
update: {},
});
for (let i = 1; i <= 4; i++) {
const documentData = await createDocumentData({ documentData: examplePdf });
await prisma.document.create({
data: {
source: DocumentSource.DOCUMENT,
title: 'Example Document',
documentDataId: examplePdfData.id,
userId: exampleUser.id,
recipients: {
create: {
name: String(adminUser.name),
email: adminUser.email,
token: Math.random().toString(36).slice(2, 9),
await prisma.document.create({
data: {
source: DocumentSource.DOCUMENT,
title: `Example Document ${i}`,
documentDataId: documentData.id,
userId: exampleUser.id,
recipients: {
create: {
name: String(adminUser.name),
email: adminUser.email,
token: Math.random().toString(36).slice(2, 9),
},
},
},
});
}
for (let i = 1; i <= 4; i++) {
const documentData = await createDocumentData({ documentData: examplePdf });
await prisma.document.create({
data: {
source: DocumentSource.DOCUMENT,
title: `Document ${i}`,
documentDataId: documentData.id,
userId: adminUser.id,
recipients: {
create: {
name: String(exampleUser.name),
email: exampleUser.email,
token: Math.random().toString(36).slice(2, 9),
},
},
},
});
}
await seedPendingDocument(exampleUser, [adminUser], {
key: 'example-pending',
createDocumentOptions: {
title: 'Pending Document',
},
});
await seedPendingDocument(adminUser, [exampleUser], {
key: 'admin-pending',
createDocumentOptions: {
title: 'Pending Document',
},
});
await Promise.all([
seedTemplate({
title: 'Template 1',
userId: exampleUser.id,
}),
seedDirectTemplate({
title: 'Direct Template 1',
userId: exampleUser.id,
}),
seedTemplate({
title: 'Template 1',
userId: adminUser.id,
}),
seedDirectTemplate({
title: 'Direct Template 1',
userId: adminUser.id,
}),
]);
const testUsers = [
'test@documenso.com',
'test2@documenso.com',

View File

@@ -0,0 +1,5 @@
import type { Field, Recipient } from '@documenso/prisma/client';
export type RecipientWithFields = Recipient & {
fields: Field[];
};

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