mirror of
https://github.com/documenso/documenso.git
synced 2025-11-18 02:32:00 +10:00
Compare commits
5 Commits
feat/custo
...
feat/dicta
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a6942f9da | |||
| 8b82d22f9f | |||
| 00e402f4cb | |||
| 1e90ca45a6 | |||
| 4189a34de0 |
@ -1,4 +1,4 @@
|
|||||||
> 🚨 We are live on Product Hunt 🎉 Check out our latest launch: <a href="https://documen.so/sign-everywhere">The Platform Plan</a>!
|
> 🚨 We are live on Product Hunt 🎉 Check out our latest launch: <a href="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-platform-plan" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=670576&theme=light" alt="Documenso Platform Plan - Whitelabeled signing flows in your product | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<a href="https://www.producthunt.com/posts/documenso-platform-plan?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-documenso-platform-plan" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=670576&theme=light" alt="Documenso Platform Plan - Whitelabeled signing flows in your product | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
|||||||
@ -14,4 +14,4 @@
|
|||||||
"public-api": "Public API",
|
"public-api": "Public API",
|
||||||
"embedding": "Embedding",
|
"embedding": "Embedding",
|
||||||
"webhooks": "Webhooks"
|
"webhooks": "Webhooks"
|
||||||
}
|
}
|
||||||
@ -6,6 +6,5 @@
|
|||||||
"solid": "Solid Integration",
|
"solid": "Solid Integration",
|
||||||
"preact": "Preact Integration",
|
"preact": "Preact Integration",
|
||||||
"angular": "Angular Integration",
|
"angular": "Angular Integration",
|
||||||
"css-variables": "CSS Variables",
|
"css-variables": "CSS Variables"
|
||||||
"web-components": "Web Components"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,9 +52,9 @@ Platform customers have access to advanced styling options to customize the embe
|
|||||||
<EmbedDirectTemplate
|
<EmbedDirectTemplate
|
||||||
token={token}
|
token={token}
|
||||||
cssVars={{
|
cssVars={{
|
||||||
primary: '#0000FF',
|
colorPrimary: '#0000FF',
|
||||||
background: '#F5F5F5',
|
colorBackground: '#F5F5F5',
|
||||||
radius: '8px',
|
borderRadius: '8px',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
@ -73,15 +73,14 @@ These customization options are available for both Direct Templates and Signing
|
|||||||
|
|
||||||
We support embedding across a range of popular JavaScript frameworks, including:
|
We support embedding across a range of popular JavaScript frameworks, including:
|
||||||
|
|
||||||
| Framework | Package |
|
| Framework | Package |
|
||||||
| --------- | ---------------------------------------------------------------------------------- |
|
| --------- | ---------------------------------------------------------------------------------- |
|
||||||
| React | [@documenso/embed-react](https://www.npmjs.com/package/@documenso/embed-react) |
|
| React | [@documenso/embed-react](https://www.npmjs.com/package/@documenso/embed-react) |
|
||||||
| Preact | [@documenso/embed-preact](https://www.npmjs.com/package/@documenso/embed-preact) |
|
| Preact | [@documenso/embed-preact](https://www.npmjs.com/package/@documenso/embed-preact) |
|
||||||
| Vue | [@documenso/embed-vue](https://www.npmjs.com/package/@documenso/embed-vue) |
|
| Vue | [@documenso/embed-vue](https://www.npmjs.com/package/@documenso/embed-vue) |
|
||||||
| Svelte | [@documenso/embed-svelte](https://www.npmjs.com/package/@documenso/embed-svelte) |
|
| Svelte | [@documenso/embed-svelte](https://www.npmjs.com/package/@documenso/embed-svelte) |
|
||||||
| Solid | [@documenso/embed-solid](https://www.npmjs.com/package/@documenso/embed-solid) |
|
| Solid | [@documenso/embed-solid](https://www.npmjs.com/package/@documenso/embed-solid) |
|
||||||
| Angular | [@documenso/embed-angular](https://www.npmjs.com/package/@documenso/embed-angular) |
|
| 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.
|
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.
|
||||||
|
|
||||||
@ -167,7 +166,6 @@ Once you've obtained the appropriate tokens, you can integrate the signing exper
|
|||||||
- [Svelte](/developers/embedding/svelte)
|
- [Svelte](/developers/embedding/svelte)
|
||||||
- [Solid](/developers/embedding/solid)
|
- [Solid](/developers/embedding/solid)
|
||||||
- [Angular](/developers/embedding/angular)
|
- [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.
|
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.
|
||||||
|
|
||||||
@ -179,5 +177,4 @@ If you're using **web components**, the integration process is slightly differen
|
|||||||
- [Solid Integration](/developers/embedding/solid)
|
- [Solid Integration](/developers/embedding/solid)
|
||||||
- [Preact Integration](/developers/embedding/preact)
|
- [Preact Integration](/developers/embedding/preact)
|
||||||
- [Angular Integration](/developers/embedding/angular)
|
- [Angular Integration](/developers/embedding/angular)
|
||||||
- [Web Components](/developers/embedding/web-components)
|
|
||||||
- [CSS Variables](/developers/embedding/css-variables)
|
- [CSS Variables](/developers/embedding/css-variables)
|
||||||
|
|||||||
@ -95,9 +95,9 @@ const MyEmbeddingComponent = () => {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
const cssVars = {
|
const cssVars = {
|
||||||
primary: '#0000FF',
|
colorPrimary: '#0000FF',
|
||||||
background: '#F5F5F5',
|
colorBackground: '#F5F5F5',
|
||||||
radius: '8px',
|
borderRadius: '8px',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -99,9 +99,9 @@ const MyEmbeddingComponent = () => {
|
|||||||
`}
|
`}
|
||||||
// CSS Variables
|
// CSS Variables
|
||||||
cssVars={{
|
cssVars={{
|
||||||
primary: '#0000FF',
|
colorPrimary: '#0000FF',
|
||||||
background: '#F5F5F5',
|
colorBackground: '#F5F5F5',
|
||||||
radius: '8px',
|
borderRadius: '8px',
|
||||||
}}
|
}}
|
||||||
// Dark Mode Control
|
// Dark Mode Control
|
||||||
darkModeDisabled={true}
|
darkModeDisabled={true}
|
||||||
|
|||||||
@ -95,9 +95,9 @@ const MyEmbeddingComponent = () => {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
const cssVars = {
|
const cssVars = {
|
||||||
primary: '#0000FF',
|
colorPrimary: '#0000FF',
|
||||||
background: '#F5F5F5',
|
colorBackground: '#F5F5F5',
|
||||||
radius: '8px',
|
borderRadius: '8px',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -97,9 +97,9 @@ Platform customers have access to advanced styling options:
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
const cssVars = {
|
const cssVars = {
|
||||||
primary: '#0000FF',
|
colorPrimary: '#0000FF',
|
||||||
background: '#F5F5F5',
|
colorBackground: '#F5F5F5',
|
||||||
radius: '8px',
|
borderRadius: '8px',
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -97,9 +97,9 @@ Platform customers have access to advanced styling options:
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
const cssVars = {
|
const cssVars = {
|
||||||
primary: '#0000FF',
|
colorPrimary: '#0000FF',
|
||||||
background: '#F5F5F5',
|
colorBackground: '#F5F5F5',
|
||||||
radius: '8px',
|
borderRadius: '8px',
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,89 +0,0 @@
|
|||||||
---
|
|
||||||
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);
|
|
||||||
```
|
|
||||||
@ -21,25 +21,14 @@ Check out the [API V1 documentation](https://app.documenso.com/api/v1/openapi) f
|
|||||||
|
|
||||||
## API V2 - Beta
|
## API V2 - Beta
|
||||||
|
|
||||||
<Callout type="warning">API V2 is currently beta, and will be subject to breaking changes</Callout>
|
Our new API V2 is currently in Beta. The new API features typed SDKs for TypeScript, Python and Go and example code for many more.
|
||||||
|
|
||||||
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.
|
<Callout type="warning">
|
||||||
|
NOW IN BETA: [API V2 Documentation](https://documen.so/api-v2-docs)
|
||||||
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>
|
</Callout>
|
||||||
|
|
||||||
🚀 [V2 Announcement](https://documen.so/sdk-blog)
|
🚀 [V2 Announcement](https://documen.so/sdk-blog)
|
||||||
|
|
||||||
📖 [Documentation](https://documen.so/api-v2-docs)
|
|
||||||
|
|
||||||
💬 [Leave Feedback](https://documen.so/sdk-feedback)
|
💬 [Leave Feedback](https://documen.so/sdk-feedback)
|
||||||
|
|
||||||
🔔 [Breaking Changes](https://documen.so/sdk-breaking)
|
🔔 [Breaking Changes](https://documen.so/sdk-breaking)
|
||||||
|
|||||||
@ -532,93 +532,3 @@ Replace the `text` value with the corresponding field type:
|
|||||||
- For the `SELECT` field it should be `select`. (check this before merge)
|
- 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.
|
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).
|
|
||||||
|
|||||||
@ -85,13 +85,12 @@ 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.
|
Documenso has 4 roles for recipients with different permissions and actions.
|
||||||
|
|
||||||
| Role | Function | Action required | Signature |
|
| Role | Function | Action required | Signature |
|
||||||
| :-------: | :-----------------------------------------------------------------------------: | :-------------: | :-------: |
|
| :------: | :-----------------------------------------------------------------------------: | :-------------: | :-------: |
|
||||||
| Signer | Needs to sign signatures fields assigned to them. | Yes | Yes |
|
| Signer | Needs to sign signatures fields assigned to them. | Yes | Yes |
|
||||||
| Approver | Needs to approve the document as a whole. Signature optional. | Yes | Optional |
|
| Approver | Needs to approve the document as a whole. Signature optional. | Yes | Optional |
|
||||||
| Viewer | Needs to confirm they viewed the document. | Yes | No |
|
| 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 |
|
| BCC | Receives a copy of the signed document after completion. No action is required. | No | No |
|
||||||
| CC | Receives a copy of the signed document after completion. No action is required. | No | No |
|
|
||||||
|
|
||||||
### Fields
|
### Fields
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/web",
|
"name": "@documenso/web",
|
||||||
"version": "1.9.1-rc.9",
|
"version": "1.9.1-rc.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -14,12 +14,6 @@ import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
|
|||||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
|
||||||
SplitButton,
|
|
||||||
SplitButtonAction,
|
|
||||||
SplitButtonDropdown,
|
|
||||||
SplitButtonDropdownItem,
|
|
||||||
} from '@documenso/ui/primitives/split-button';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DocumentPageViewButtonProps = {
|
export type DocumentPageViewButtonProps = {
|
||||||
@ -31,9 +25,7 @@ export type DocumentPageViewButtonProps = {
|
|||||||
team?: Pick<Team, 'id' | 'url'>;
|
team?: Pick<Team, 'id' | 'url'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentPageViewButton = ({
|
export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps) => {
|
||||||
document: activeDocument,
|
|
||||||
}: DocumentPageViewButtonProps) => {
|
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -42,27 +34,25 @@ export const DocumentPageViewButton = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipient = activeDocument.recipients.find(
|
const recipient = document.recipients.find((recipient) => recipient.email === session.user.email);
|
||||||
(recipient) => recipient.email === session.user.email,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isRecipient = !!recipient;
|
const isRecipient = !!recipient;
|
||||||
const isPending = activeDocument.status === DocumentStatus.PENDING;
|
const isPending = document.status === DocumentStatus.PENDING;
|
||||||
const isComplete = activeDocument.status === DocumentStatus.COMPLETED;
|
const isComplete = document.status === DocumentStatus.COMPLETED;
|
||||||
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||||
const role = recipient?.role;
|
const role = recipient?.role;
|
||||||
|
|
||||||
const documentsPath = formatDocumentsPath(activeDocument.team?.url);
|
const documentsPath = formatDocumentsPath(document.team?.url);
|
||||||
|
|
||||||
const onDownloadClick = async () => {
|
const onDownloadClick = async () => {
|
||||||
try {
|
try {
|
||||||
const documentWithData = await trpcClient.document.getDocumentById.query(
|
const documentWithData = await trpcClient.document.getDocumentById.query(
|
||||||
{
|
{
|
||||||
documentId: activeDocument.id,
|
documentId: document.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
context: {
|
context: {
|
||||||
teamId: activeDocument.team?.id?.toString(),
|
teamId: document.team?.id?.toString(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -73,10 +63,7 @@ export const DocumentPageViewButton = ({
|
|||||||
throw new Error('No document available');
|
throw new Error('No document available');
|
||||||
}
|
}
|
||||||
|
|
||||||
await downloadPDF({
|
await downloadPDF({ documentData, fileName: documentWithData.title });
|
||||||
documentData,
|
|
||||||
fileName: documentWithData.title,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Something went wrong`),
|
title: _(msg`Something went wrong`),
|
||||||
@ -86,100 +73,6 @@ export const DocumentPageViewButton = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDownloadAuditLogClick = async () => {
|
|
||||||
try {
|
|
||||||
const { url } = await trpcClient.document.downloadAuditLogs.mutate({
|
|
||||||
documentId: activeDocument.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const iframe = Object.assign(document.createElement('iframe'), {
|
|
||||||
src: url,
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.assign(iframe.style, {
|
|
||||||
position: 'fixed',
|
|
||||||
top: '0',
|
|
||||||
left: '0',
|
|
||||||
width: '0',
|
|
||||||
height: '0',
|
|
||||||
});
|
|
||||||
|
|
||||||
const onLoaded = () => {
|
|
||||||
if (iframe.contentDocument?.readyState === 'complete') {
|
|
||||||
iframe.contentWindow?.print();
|
|
||||||
|
|
||||||
iframe.contentWindow?.addEventListener('afterprint', () => {
|
|
||||||
document.body.removeChild(iframe);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// When the iframe has loaded, print the iframe and remove it from the dom
|
|
||||||
iframe.addEventListener('load', onLoaded);
|
|
||||||
|
|
||||||
document.body.appendChild(iframe);
|
|
||||||
|
|
||||||
onLoaded();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: _(msg`Something went wrong`),
|
|
||||||
description: _(
|
|
||||||
msg`Sorry, we were unable to download the audit logs. Please try again later.`,
|
|
||||||
),
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDownloadSigningCertificateClick = async () => {
|
|
||||||
try {
|
|
||||||
const { url } = await trpcClient.document.downloadCertificate.mutate({
|
|
||||||
documentId: activeDocument.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const iframe = Object.assign(document.createElement('iframe'), {
|
|
||||||
src: url,
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.assign(iframe.style, {
|
|
||||||
position: 'fixed',
|
|
||||||
top: '0',
|
|
||||||
left: '0',
|
|
||||||
width: '0',
|
|
||||||
height: '0',
|
|
||||||
});
|
|
||||||
|
|
||||||
const onLoaded = () => {
|
|
||||||
if (iframe.contentDocument?.readyState === 'complete') {
|
|
||||||
iframe.contentWindow?.print();
|
|
||||||
|
|
||||||
iframe.contentWindow?.addEventListener('afterprint', () => {
|
|
||||||
document.body.removeChild(iframe);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// When the iframe has loaded, print the iframe and remove it from the dom
|
|
||||||
iframe.addEventListener('load', onLoaded);
|
|
||||||
|
|
||||||
document.body.appendChild(iframe);
|
|
||||||
|
|
||||||
onLoaded();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: _(msg`Something went wrong`),
|
|
||||||
description: _(
|
|
||||||
msg`Sorry, we were unable to download the certificate. Please try again later.`,
|
|
||||||
),
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return match({
|
return match({
|
||||||
isRecipient,
|
isRecipient,
|
||||||
isPending,
|
isPending,
|
||||||
@ -213,27 +106,16 @@ export const DocumentPageViewButton = ({
|
|||||||
))
|
))
|
||||||
.with({ isComplete: false }, () => (
|
.with({ isComplete: false }, () => (
|
||||||
<Button className="w-full" asChild>
|
<Button className="w-full" asChild>
|
||||||
<Link href={`${documentsPath}/${activeDocument.id}/edit`}>
|
<Link href={`${documentsPath}/${document.id}/edit`}>
|
||||||
<Trans>Edit</Trans>
|
<Trans>Edit</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
))
|
))
|
||||||
.with({ isComplete: true }, () => (
|
.with({ isComplete: true }, () => (
|
||||||
<SplitButton className="flex w-full">
|
<Button className="w-full" onClick={onDownloadClick}>
|
||||||
<SplitButtonAction className="w-full" onClick={() => void onDownloadClick()}>
|
<Download className="-ml-1 mr-2 inline h-4 w-4" />
|
||||||
<Download className="-ml-1 mr-2 inline h-4 w-4" />
|
<Trans>Download</Trans>
|
||||||
<Trans>Download</Trans>
|
</Button>
|
||||||
</SplitButtonAction>
|
|
||||||
<SplitButtonDropdown>
|
|
||||||
<SplitButtonDropdownItem onClick={() => void onDownloadAuditLogClick()}>
|
|
||||||
<Trans>Only Audit Log</Trans>
|
|
||||||
</SplitButtonDropdownItem>
|
|
||||||
|
|
||||||
<SplitButtonDropdownItem onClick={() => void onDownloadSigningCertificateClick()}>
|
|
||||||
<Trans>Only Signing Certificate</Trans>
|
|
||||||
</SplitButtonDropdownItem>
|
|
||||||
</SplitButtonDropdown>
|
|
||||||
</SplitButton>
|
|
||||||
))
|
))
|
||||||
.otherwise(() => null);
|
.otherwise(() => null);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -73,7 +73,7 @@ export const EditDocumentForm = ({
|
|||||||
|
|
||||||
const { recipients, fields } = document;
|
const { recipients, fields } = document;
|
||||||
|
|
||||||
const { mutateAsync: updateDocument } = trpc.document.setSettingsForDocument.useMutation({
|
const { mutateAsync: updateDocument } = trpc.document.updateDocument.useMutation({
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
onSuccess: (newData) => {
|
onSuccess: (newData) => {
|
||||||
utils.document.getDocumentWithDetailsById.setData(
|
utils.document.getDocumentWithDetailsById.setData(
|
||||||
@ -85,19 +85,6 @@ export const EditDocumentForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: setSigningOrderForDocument } =
|
|
||||||
trpc.document.setSigningOrderForDocument.useMutation({
|
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
|
||||||
onSuccess: (newData) => {
|
|
||||||
utils.document.getDocumentWithDetailsById.setData(
|
|
||||||
{
|
|
||||||
documentId: initialDocument.id,
|
|
||||||
},
|
|
||||||
(oldData) => ({ ...(oldData || initialDocument), ...newData, id: Number(newData.id) }),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutateAsync: addFields } = trpc.field.addFields.useMutation({
|
const { mutateAsync: addFields } = trpc.field.addFields.useMutation({
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
onSuccess: ({ fields: newFields }) => {
|
onSuccess: ({ fields: newFields }) => {
|
||||||
@ -187,8 +174,6 @@ export const EditDocumentForm = ({
|
|||||||
title: data.title,
|
title: data.title,
|
||||||
externalId: data.externalId || null,
|
externalId: data.externalId || null,
|
||||||
visibility: data.visibility,
|
visibility: data.visibility,
|
||||||
includeSigningCertificate: data.includeSigningCertificate,
|
|
||||||
includeAuditTrailLog: data.includeAuditTrailLog,
|
|
||||||
globalAccessAuth: data.globalAccessAuth ?? null,
|
globalAccessAuth: data.globalAccessAuth ?? null,
|
||||||
globalActionAuth: data.globalActionAuth ?? null,
|
globalActionAuth: data.globalActionAuth ?? null,
|
||||||
},
|
},
|
||||||
@ -218,9 +203,12 @@ export const EditDocumentForm = ({
|
|||||||
const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => {
|
const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
setSigningOrderForDocument({
|
updateDocument({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
signingOrder: data.signingOrder,
|
meta: {
|
||||||
|
signingOrder: data.signingOrder,
|
||||||
|
modifyNextSigner: data.modifyNextSigner,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setRecipients({
|
setRecipients({
|
||||||
@ -393,6 +381,7 @@ export const EditDocumentForm = ({
|
|||||||
documentFlow={documentFlow.signers}
|
documentFlow={documentFlow.signers}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
signingOrder={document.documentMeta?.signingOrder}
|
signingOrder={document.documentMeta?.signingOrder}
|
||||||
|
modifyNextSigner={document.documentMeta?.modifyNextSigner}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
isDocumentEnterprise={isDocumentEnterprise}
|
isDocumentEnterprise={isDocumentEnterprise}
|
||||||
onSubmit={onAddSignersFormSubmit}
|
onSubmit={onAddSignersFormSubmit}
|
||||||
|
|||||||
@ -119,7 +119,7 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
|
|||||||
<Trans>Document</Trans>
|
<Trans>Document</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col justify-between truncate sm:flex-row">
|
||||||
<div>
|
<div>
|
||||||
<h1
|
<h1
|
||||||
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
|
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
|
||||||
@ -127,8 +127,7 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
|
|||||||
>
|
>
|
||||||
{document.title}
|
{document.title}
|
||||||
</h1>
|
</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">
|
<div className="mt-2.5 flex items-center gap-x-6">
|
||||||
<DocumentStatusComponent
|
<DocumentStatusComponent
|
||||||
inheritColor
|
inheritColor
|
||||||
@ -136,16 +135,17 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
|
|||||||
className="text-muted-foreground"
|
className="text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex w-full flex-row sm:mt-0 sm:w-auto sm:self-end">
|
</div>
|
||||||
<DownloadCertificateButton
|
|
||||||
className="mr-2"
|
|
||||||
documentId={document.id}
|
|
||||||
documentStatus={document.status}
|
|
||||||
teamId={team?.id}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DownloadAuditLogButton teamId={team?.id} documentId={document.id} />
|
<div className="mt-4 flex w-full flex-row sm:mt-0 sm:w-auto sm:self-end">
|
||||||
</div>
|
<DownloadCertificateButton
|
||||||
|
className="mr-2"
|
||||||
|
documentId={document.id}
|
||||||
|
documentStatus={document.status}
|
||||||
|
teamId={team?.id}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DownloadAuditLogButton teamId={team?.id} documentId={document.id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -154,7 +154,7 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
|
|||||||
{documentInformation.map((info, i) => (
|
{documentInformation.map((info, i) => (
|
||||||
<div className="text-foreground text-sm" key={i}>
|
<div className="text-foreground text-sm" key={i}>
|
||||||
<h3 className="font-semibold">{_(info.description)}</h3>
|
<h3 className="font-semibold">{_(info.description)}</h3>
|
||||||
<p className="text-muted-foreground truncate">{info.value}</p>
|
<p className="text-muted-foreground">{info.value}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
@ -76,6 +76,7 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
|
|||||||
? {
|
? {
|
||||||
...templateMeta,
|
...templateMeta,
|
||||||
signingOrder: templateMeta.signingOrder || DocumentSigningOrder.SEQUENTIAL,
|
signingOrder: templateMeta.signingOrder || DocumentSigningOrder.SEQUENTIAL,
|
||||||
|
modifyNextSigner: templateMeta.modifyNextSigner ?? false,
|
||||||
documentId: 0,
|
documentId: 0,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@ -90,7 +90,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
|
|
||||||
const tempField: DirectTemplateLocalField = {
|
const tempField: DirectTemplateLocalField = {
|
||||||
...field,
|
...field,
|
||||||
customText: value.value ?? '',
|
customText: value.value,
|
||||||
inserted: true,
|
inserted: true,
|
||||||
signedValue: value,
|
signedValue: value,
|
||||||
};
|
};
|
||||||
@ -101,8 +101,8 @@ export const SignDirectTemplateForm = ({
|
|||||||
created: new Date(),
|
created: new Date(),
|
||||||
recipientId: 1,
|
recipientId: 1,
|
||||||
fieldId: 1,
|
fieldId: 1,
|
||||||
signatureImageAsBase64: value.value?.startsWith('data:') ? value.value : null,
|
signatureImageAsBase64: value.value.startsWith('data:') ? value.value : null,
|
||||||
typedSignature: value.value && !value.value.startsWith('data:') ? value.value : null,
|
typedSignature: value.value.startsWith('data:') ? null : value.value,
|
||||||
} satisfies Signature;
|
} satisfies Signature;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -44,12 +44,7 @@ export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFie
|
|||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
|
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
|
||||||
|
|
||||||
const parsedFieldMeta = ZCheckboxFieldMeta.parse(
|
const parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
||||||
field.fieldMeta ?? {
|
|
||||||
type: 'checkbox',
|
|
||||||
values: [{ id: 1, checked: false, value: '' }],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const values = parsedFieldMeta.values?.map((item) => ({
|
const values = parsedFieldMeta.values?.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
|
|||||||
@ -15,7 +15,12 @@ import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
|||||||
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
import { type Field, FieldType, RecipientRole } from '@documenso/prisma/client';
|
import {
|
||||||
|
DocumentSigningOrder,
|
||||||
|
type Field,
|
||||||
|
FieldType,
|
||||||
|
RecipientRole,
|
||||||
|
} from '@documenso/prisma/client';
|
||||||
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||||
@ -40,6 +45,12 @@ export type SigningFormProps = {
|
|||||||
isRecipientsTurn: boolean;
|
isRecipientsTurn: boolean;
|
||||||
allRecipients?: RecipientWithFields[];
|
allRecipients?: RecipientWithFields[];
|
||||||
setSelectedSignerId?: (id: number | null) => void;
|
setSelectedSignerId?: (id: number | null) => void;
|
||||||
|
isLastRecipient: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SigningFormData = {
|
||||||
|
email?: string;
|
||||||
|
name?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SigningForm = ({
|
export const SigningForm = ({
|
||||||
@ -50,6 +61,7 @@ export const SigningForm = ({
|
|||||||
isRecipientsTurn,
|
isRecipientsTurn,
|
||||||
allRecipients = [],
|
allRecipients = [],
|
||||||
setSelectedSignerId,
|
setSelectedSignerId,
|
||||||
|
isLastRecipient,
|
||||||
}: SigningFormProps) => {
|
}: SigningFormProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@ -77,7 +89,7 @@ export const SigningForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { handleSubmit, formState } = useForm();
|
const { handleSubmit, formState } = useForm<SigningFormData>();
|
||||||
|
|
||||||
// Keep the loading state going if successful since the redirect may take some time.
|
// Keep the loading state going if successful since the redirect may take some time.
|
||||||
const isSubmitting = formState.isSubmitting || formState.isSubmitSuccessful;
|
const isSubmitting = formState.isSubmitting || formState.isSubmitSuccessful;
|
||||||
@ -102,20 +114,58 @@ export const SigningForm = ({
|
|||||||
validateFieldsInserted(fieldsRequiringValidation);
|
validateFieldsInserted(fieldsRequiringValidation);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFormSubmit = async () => {
|
const completeDocument = async (
|
||||||
setValidateUninsertedFields(true);
|
authOptions?: TRecipientActionAuth,
|
||||||
|
nextSigner?: { email: string; name: string },
|
||||||
|
) => {
|
||||||
|
const payload = {
|
||||||
|
token: recipient.token,
|
||||||
|
documentId: document.id,
|
||||||
|
authOptions,
|
||||||
|
...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation);
|
await completeDocumentWithToken(payload);
|
||||||
|
|
||||||
if (hasSignatureField && !signatureValid) {
|
analytics.capture('App: Recipient has completed signing', {
|
||||||
return;
|
signerId: recipient.id,
|
||||||
|
documentId: document.id,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
redirectUrl ? router.push(redirectUrl) : router.push(`/sign/${recipient.token}/complete`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFormSubmit = async (data: SigningFormData) => {
|
||||||
|
try {
|
||||||
|
setValidateUninsertedFields(true);
|
||||||
|
|
||||||
|
const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation);
|
||||||
|
|
||||||
|
if (hasSignatureField && !signatureValid) {
|
||||||
|
throw new Error('Please provide a valid signature');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFieldsValid) {
|
||||||
|
throw new Error('Please complete all required fields');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSigner =
|
||||||
|
data.email && data.name
|
||||||
|
? {
|
||||||
|
email: data.email,
|
||||||
|
name: data.name,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
await completeDocument(undefined, nextSigner);
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'An error occurred while completing the document. Please try again.',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isFieldsValid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await completeDocument();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAssistantFormSubmit = () => {
|
const onAssistantFormSubmit = () => {
|
||||||
@ -143,22 +193,6 @@ export const SigningForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const completeDocument = async (authOptions?: TRecipientActionAuth) => {
|
|
||||||
await completeDocumentWithToken({
|
|
||||||
token: recipient.token,
|
|
||||||
documentId: document.id,
|
|
||||||
authOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
analytics.capture('App: Recipient has completed signing', {
|
|
||||||
signerId: recipient.id,
|
|
||||||
documentId: document.id,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
redirectUrl ? router.push(redirectUrl) : router.push(`/sign/${recipient.token}/complete`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -208,12 +242,21 @@ export const SigningForm = ({
|
|||||||
|
|
||||||
<SignDialog
|
<SignDialog
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
onSignatureComplete={handleSubmit(onFormSubmit)}
|
onSignatureComplete={async (nextSigner) => {
|
||||||
|
await handleSubmit(async (formData) =>
|
||||||
|
onFormSubmit({ ...formData, ...nextSigner }),
|
||||||
|
)();
|
||||||
|
}}
|
||||||
documentTitle={document.title}
|
documentTitle={document.title}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
fieldsValidated={fieldsValidated}
|
fieldsValidated={fieldsValidated}
|
||||||
role={recipient.role}
|
role={recipient.role}
|
||||||
disabled={!isRecipientsTurn}
|
disabled={!isRecipientsTurn}
|
||||||
|
canModifyNextSigner={
|
||||||
|
document.documentMeta?.modifyNextSigner &&
|
||||||
|
document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL &&
|
||||||
|
!isLastRecipient
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -311,11 +354,7 @@ export const SigningForm = ({
|
|||||||
<>
|
<>
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
{recipient.role === RecipientRole.APPROVER && !hasSignatureField ? (
|
<Trans>Please review the document before signing.</Trans>
|
||||||
<Trans>Please review the document before approving.</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans>Please review the document before signing.</Trans>
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr className="border-border mb-8 mt-4" />
|
<hr className="border-border mb-8 mt-4" />
|
||||||
@ -339,40 +378,38 @@ export const SigningForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasSignatureField && (
|
<div>
|
||||||
<div>
|
<Label htmlFor="Signature">
|
||||||
<Label htmlFor="Signature">
|
<Trans>Signature</Trans>
|
||||||
<Trans>Signature</Trans>
|
</Label>
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Card className="mt-2" gradient degrees={-120}>
|
<Card className="mt-2" gradient degrees={-120}>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<SignaturePad
|
<SignaturePad
|
||||||
className="h-44 w-full"
|
className="h-44 w-full"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
defaultValue={signature ?? undefined}
|
defaultValue={signature ?? undefined}
|
||||||
onValidityChange={(isValid) => {
|
onValidityChange={(isValid) => {
|
||||||
setSignatureValid(isValid);
|
setSignatureValid(isValid);
|
||||||
}}
|
}}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
if (signatureValid) {
|
if (signatureValid) {
|
||||||
setSignature(value);
|
setSignature(value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
allowTypedSignature={document.documentMeta?.typedSignatureEnabled}
|
allowTypedSignature={document.documentMeta?.typedSignatureEnabled}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{!signatureValid && (
|
{hasSignatureField && !signatureValid && (
|
||||||
<div className="text-destructive mt-2 text-sm">
|
<div className="text-destructive mt-2 text-sm">
|
||||||
<Trans>
|
<Trans>
|
||||||
Signature is too small. Please provide a more complete signature.
|
Signature is too small. Please provide a more complete signature.
|
||||||
</Trans>
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 md:flex-row">
|
<div className="flex flex-col gap-4 md:flex-row">
|
||||||
@ -389,12 +426,21 @@ export const SigningForm = ({
|
|||||||
|
|
||||||
<SignDialog
|
<SignDialog
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
onSignatureComplete={handleSubmit(onFormSubmit)}
|
onSignatureComplete={async (nextSigner) => {
|
||||||
|
await handleSubmit(async (formData) =>
|
||||||
|
onFormSubmit({ ...formData, ...nextSigner }),
|
||||||
|
)();
|
||||||
|
}}
|
||||||
documentTitle={document.title}
|
documentTitle={document.title}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
fieldsValidated={fieldsValidated}
|
fieldsValidated={fieldsValidated}
|
||||||
role={recipient.role}
|
role={recipient.role}
|
||||||
disabled={!isRecipientsTurn}
|
disabled={!isRecipientsTurn}
|
||||||
|
canModifyNextSigner={
|
||||||
|
document.documentMeta?.modifyNextSigner &&
|
||||||
|
document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL &&
|
||||||
|
!isLastRecipient
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-re
|
|||||||
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
||||||
import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get-completed-fields-for-token';
|
import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get-completed-fields-for-token';
|
||||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||||
|
import { getIsLastRecipient } from '@documenso/lib/server-only/recipient/get-is-last-recipient';
|
||||||
import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn';
|
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 { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||||
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||||
@ -44,7 +45,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
|
|
||||||
const requestMetadata = extractNextHeaderRequestMetadata(requestHeaders);
|
const requestMetadata = extractNextHeaderRequestMetadata(requestHeaders);
|
||||||
|
|
||||||
const [document, recipient, fields, completedFields] = await Promise.all([
|
const [document, recipient, fields, completedFields, isLastRecipient] = await Promise.all([
|
||||||
getDocumentAndSenderByToken({
|
getDocumentAndSenderByToken({
|
||||||
token,
|
token,
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
@ -53,6 +54,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
getRecipientByToken({ token }).catch(() => null),
|
getRecipientByToken({ token }).catch(() => null),
|
||||||
getFieldsForToken({ token }),
|
getFieldsForToken({ token }),
|
||||||
getCompletedFieldsForToken({ token }),
|
getCompletedFieldsForToken({ token }),
|
||||||
|
getIsLastRecipient({ token }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -169,6 +171,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
completedFields={completedFields}
|
completedFields={completedFields}
|
||||||
isRecipientsTurn={isRecipientsTurn}
|
isRecipientsTurn={isRecipientsTurn}
|
||||||
allRecipients={allRecipients}
|
allRecipients={allRecipients}
|
||||||
|
isLastRecipient={isLastRecipient}
|
||||||
/>
|
/>
|
||||||
</DocumentAuthProvider>
|
</DocumentAuthProvider>
|
||||||
</SigningProvider>
|
</SigningProvider>
|
||||||
|
|||||||
@ -37,11 +37,6 @@ export const RecipientProvider = ({
|
|||||||
recipient,
|
recipient,
|
||||||
targetSigner = null,
|
targetSigner = null,
|
||||||
}: RecipientProviderProps) => {
|
}: RecipientProviderProps) => {
|
||||||
// console.log({
|
|
||||||
// recipient,
|
|
||||||
// targetSigner,
|
|
||||||
// isAssistantMode: !!targetSigner,
|
|
||||||
// });
|
|
||||||
return (
|
return (
|
||||||
<RecipientContext.Provider
|
<RecipientContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
|||||||
@ -43,10 +43,9 @@ type TRejectDocumentFormSchema = z.infer<typeof ZRejectDocumentFormSchema>;
|
|||||||
export interface RejectDocumentDialogProps {
|
export interface RejectDocumentDialogProps {
|
||||||
document: Pick<Document, 'id'>;
|
document: Pick<Document, 'id'>;
|
||||||
token: string;
|
token: string;
|
||||||
onRejected?: (reason: string) => void | Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RejectDocumentDialog({ document, token, onRejected }: RejectDocumentDialogProps) {
|
export function RejectDocumentDialog({ document, token }: RejectDocumentDialogProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@ -80,11 +79,7 @@ export function RejectDocumentDialog({ document, token, onRejected }: RejectDocu
|
|||||||
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
|
||||||
if (onRejected) {
|
router.push(`/sign/${token}/rejected`);
|
||||||
await onRejected(reason);
|
|
||||||
} else {
|
|
||||||
router.push(`/sign/${token}/rejected`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
|
|||||||
@ -1,19 +1,36 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
import { match } from 'ts-pattern';
|
import { ArrowRight } from 'lucide-react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
|
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||||
import type { Field } from '@documenso/prisma/client';
|
import type { Field } from '@documenso/prisma/client';
|
||||||
import { RecipientRole } from '@documenso/prisma/client';
|
import { RecipientRole } from '@documenso/prisma/client';
|
||||||
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@documenso/ui/primitives/form/form';
|
||||||
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
|
||||||
import { SigningDisclosure } from '~/components/general/signing-disclosure';
|
import { SigningDisclosure } from '~/components/general/signing-disclosure';
|
||||||
|
|
||||||
@ -22,12 +39,26 @@ export type SignDialogProps = {
|
|||||||
documentTitle: string;
|
documentTitle: string;
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
fieldsValidated: () => void | Promise<void>;
|
fieldsValidated: () => void | Promise<void>;
|
||||||
onSignatureComplete: () => void | Promise<void>;
|
onSignatureComplete: (nextSigner?: { email?: string; name?: string }) => void | Promise<void>;
|
||||||
role: RecipientRole;
|
role: RecipientRole;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
canModifyNextSigner?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SignDialog = ({
|
const formSchema = z.object({
|
||||||
|
modifyNextSigner: z.boolean().default(false),
|
||||||
|
nextSigner: z
|
||||||
|
.object({
|
||||||
|
email: z.string().email({ message: 'Please enter a valid email address' }).optional(),
|
||||||
|
name: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.default({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type TFormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export function SignDialog({
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
documentTitle,
|
documentTitle,
|
||||||
fields,
|
fields,
|
||||||
@ -35,7 +66,9 @@ export const SignDialog = ({
|
|||||||
onSignatureComplete,
|
onSignatureComplete,
|
||||||
role,
|
role,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}: SignDialogProps) => {
|
canModifyNextSigner = false,
|
||||||
|
}: SignDialogProps) {
|
||||||
|
const [step, setStep] = useState(1);
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
|
|
||||||
const isComplete = useMemo(() => !fieldsContainUnsignedRequiredField(fields), [fields]);
|
const isComplete = useMemo(() => !fieldsContainUnsignedRequiredField(fields), [fields]);
|
||||||
@ -48,134 +81,336 @@ export const SignDialog = ({
|
|||||||
setShowDialog(open);
|
setShowDialog(open);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const totalSteps = 2;
|
||||||
|
|
||||||
|
const handleContinue = () => {
|
||||||
|
if (step < totalSteps) {
|
||||||
|
setStep(step + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const form = useForm<TFormSchema>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onFormSubmit = async (data: TFormSchema) => {
|
||||||
|
try {
|
||||||
|
await fieldsValidated();
|
||||||
|
|
||||||
|
await onSignatureComplete({
|
||||||
|
email: data.nextSigner.email?.trim().toLowerCase(),
|
||||||
|
name: data.nextSigner.name?.trim(),
|
||||||
|
});
|
||||||
|
|
||||||
|
setShowDialog(false);
|
||||||
|
form.reset();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={showDialog} onOpenChange={handleOpenChange}>
|
<>
|
||||||
<DialogTrigger asChild>
|
{!canModifyNextSigner ? (
|
||||||
<Button
|
<Dialog open={showDialog} onOpenChange={handleOpenChange}>
|
||||||
className="w-full"
|
<DialogTrigger asChild>
|
||||||
type="button"
|
|
||||||
size="lg"
|
|
||||||
onClick={fieldsValidated}
|
|
||||||
loading={isSubmitting}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{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">
|
|
||||||
{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]">
|
|
||||||
{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>
|
|
||||||
<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>
|
|
||||||
<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>
|
|
||||||
<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" />
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
|
||||||
<Button
|
<Button
|
||||||
|
className="w-full"
|
||||||
type="button"
|
type="button"
|
||||||
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
|
size="lg"
|
||||||
variant="secondary"
|
onClick={fieldsValidated}
|
||||||
onClick={() => {
|
|
||||||
setShowDialog(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trans>Cancel</Trans>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="flex-1"
|
|
||||||
disabled={!isComplete}
|
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
onClick={onSignatureComplete}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{match(role)
|
{isComplete ? <Trans>Complete</Trans> : <Trans>Next field</Trans>}
|
||||||
.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>
|
</Button>
|
||||||
</div>
|
</DialogTrigger>
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
<DialogContent>
|
||||||
</Dialog>
|
<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>}
|
||||||
|
</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}
|
||||||
|
</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}
|
||||||
|
</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}"
|
||||||
|
</span>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
<br /> Are you sure?
|
||||||
|
</Trans>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SigningDisclosure className="mt-4" />
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setShowDialog(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="flex-1"
|
||||||
|
disabled={!isComplete}
|
||||||
|
loading={isSubmitting}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await onSignatureComplete();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{role === RecipientRole.VIEWER && <Trans>Mark as Viewed</Trans>}
|
||||||
|
{role === RecipientRole.SIGNER && <Trans>Sign</Trans>}
|
||||||
|
{role === RecipientRole.APPROVER && <Trans>Approve</Trans>}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
) : (
|
||||||
|
<Dialog
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (open) setStep(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
type="button"
|
||||||
|
size="lg"
|
||||||
|
onClick={fieldsValidated}
|
||||||
|
loading={isSubmitting}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{isComplete ? <Trans>Complete</Trans> : <Trans>Next field</Trans>}
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogTitle>
|
||||||
|
{step === 1 && (
|
||||||
|
<div className="text-foreground text-base font-semibold">
|
||||||
|
<Trans>
|
||||||
|
Modify Next Signer <span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 2 && (
|
||||||
|
<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>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
{step === 1 && (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onFormSubmit)} className="flex flex-col gap-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="modifyNextSigner"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="font-normal">
|
||||||
|
<Trans>Modify next signer details</Trans>
|
||||||
|
</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{form.watch('modifyNextSigner') && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="nextSigner.email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
<Trans>Next Signer Email</Trans>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="email" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="nextSigner.name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
<Trans>Next Signer Name</Trans>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 2 && (
|
||||||
|
<>
|
||||||
|
<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}
|
||||||
|
</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}
|
||||||
|
</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}"
|
||||||
|
</span>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
<br /> Are you sure?
|
||||||
|
</Trans>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SigningDisclosure className="mt-4" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
|
||||||
|
<div className="flex justify-center space-x-1.5 max-sm:order-1">
|
||||||
|
{[...Array(totalSteps)].map((_, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => setStep(index + 1)}
|
||||||
|
className={cn(
|
||||||
|
'bg-primary h-1.5 w-1.5 rounded-full',
|
||||||
|
index + 1 === step ? 'bg-primary' : 'opacity-20',
|
||||||
|
)}
|
||||||
|
type="button"
|
||||||
|
aria-label={`Go to step ${index + 1}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button type="button" variant="ghost">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
|
||||||
|
{step === 1 && (
|
||||||
|
<Button className="group" type="button" onClick={handleContinue}>
|
||||||
|
Next
|
||||||
|
<ArrowRight
|
||||||
|
className="-me-1 ms-2 opacity-60 transition-transform group-hover:translate-x-0.5"
|
||||||
|
size={16}
|
||||||
|
strokeWidth={2}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 2 && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="flex-1"
|
||||||
|
disabled={!isComplete}
|
||||||
|
loading={isSubmitting}
|
||||||
|
onClick={form.handleSubmit(onFormSubmit)}
|
||||||
|
>
|
||||||
|
{role === RecipientRole.VIEWER && <Trans>Mark as Viewed</Trans>}
|
||||||
|
{role === RecipientRole.SIGNER && <Trans>Sign</Trans>}
|
||||||
|
{role === RecipientRole.APPROVER && <Trans>Approve</Trans>}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|||||||
@ -182,23 +182,6 @@ export const SigningFieldContainer = ({
|
|||||||
</button>
|
</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}
|
{children}
|
||||||
</FieldRootContainer>
|
</FieldRootContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -49,6 +49,7 @@ export type SigningPageViewProps = {
|
|||||||
completedFields: CompletedField[];
|
completedFields: CompletedField[];
|
||||||
isRecipientsTurn: boolean;
|
isRecipientsTurn: boolean;
|
||||||
allRecipients?: RecipientWithFields[];
|
allRecipients?: RecipientWithFields[];
|
||||||
|
isLastRecipient: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SigningPageView = ({
|
export const SigningPageView = ({
|
||||||
@ -58,6 +59,7 @@ export const SigningPageView = ({
|
|||||||
completedFields,
|
completedFields,
|
||||||
isRecipientsTurn,
|
isRecipientsTurn,
|
||||||
allRecipients = [],
|
allRecipients = [],
|
||||||
|
isLastRecipient,
|
||||||
}: SigningPageViewProps) => {
|
}: SigningPageViewProps) => {
|
||||||
const { documentData, documentMeta } = document;
|
const { documentData, documentMeta } = document;
|
||||||
|
|
||||||
@ -159,6 +161,7 @@ export const SigningPageView = ({
|
|||||||
redirectUrl={documentMeta?.redirectUrl}
|
redirectUrl={documentMeta?.redirectUrl}
|
||||||
isRecipientsTurn={isRecipientsTurn}
|
isRecipientsTurn={isRecipientsTurn}
|
||||||
allRecipients={allRecipients}
|
allRecipients={allRecipients}
|
||||||
|
isLastRecipient={isLastRecipient}
|
||||||
setSelectedSignerId={setSelectedSignerId}
|
setSelectedSignerId={setSelectedSignerId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -41,7 +41,6 @@ const ZTeamDocumentPreferencesFormSchema = z.object({
|
|||||||
includeSenderDetails: z.boolean(),
|
includeSenderDetails: z.boolean(),
|
||||||
typedSignatureEnabled: z.boolean(),
|
typedSignatureEnabled: z.boolean(),
|
||||||
includeSigningCertificate: z.boolean(),
|
includeSigningCertificate: z.boolean(),
|
||||||
includeAuditTrailLog: z.boolean(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type TTeamDocumentPreferencesFormSchema = z.infer<typeof ZTeamDocumentPreferencesFormSchema>;
|
type TTeamDocumentPreferencesFormSchema = z.infer<typeof ZTeamDocumentPreferencesFormSchema>;
|
||||||
@ -73,7 +72,6 @@ export const TeamDocumentPreferencesForm = ({
|
|||||||
includeSenderDetails: settings?.includeSenderDetails ?? false,
|
includeSenderDetails: settings?.includeSenderDetails ?? false,
|
||||||
typedSignatureEnabled: settings?.typedSignatureEnabled ?? true,
|
typedSignatureEnabled: settings?.typedSignatureEnabled ?? true,
|
||||||
includeSigningCertificate: settings?.includeSigningCertificate ?? true,
|
includeSigningCertificate: settings?.includeSigningCertificate ?? true,
|
||||||
includeAuditTrailLog: settings?.includeAuditTrailLog ?? false,
|
|
||||||
},
|
},
|
||||||
resolver: zodResolver(ZTeamDocumentPreferencesFormSchema),
|
resolver: zodResolver(ZTeamDocumentPreferencesFormSchema),
|
||||||
});
|
});
|
||||||
@ -88,7 +86,6 @@ export const TeamDocumentPreferencesForm = ({
|
|||||||
includeSenderDetails,
|
includeSenderDetails,
|
||||||
includeSigningCertificate,
|
includeSigningCertificate,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
includeAuditTrailLog,
|
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
await updateTeamDocumentPreferences({
|
await updateTeamDocumentPreferences({
|
||||||
@ -99,7 +96,6 @@ export const TeamDocumentPreferencesForm = ({
|
|||||||
includeSenderDetails,
|
includeSenderDetails,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
includeSigningCertificate,
|
includeSigningCertificate,
|
||||||
includeAuditTrailLog,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -304,37 +300,6 @@ export const TeamDocumentPreferencesForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="includeAuditTrailLog"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex-1">
|
|
||||||
<FormLabel>
|
|
||||||
<Trans>Include the Audit Trail Log in the Document</Trans>
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<FormControl className="block">
|
|
||||||
<Switch
|
|
||||||
ref={field.ref}
|
|
||||||
name={field.name}
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormDescription>
|
|
||||||
<Trans>
|
|
||||||
Controls whether the audit trail log will be included in the document when it is
|
|
||||||
downloaded. The audit trail log can still be downloaded from the logs page
|
|
||||||
separately.
|
|
||||||
</Trans>
|
|
||||||
</FormDescription>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex flex-row justify-end space-x-4">
|
<div className="flex flex-row justify-end space-x-4">
|
||||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||||
<Trans>Save</Trans>
|
<Trans>Save</Trans>
|
||||||
|
|||||||
@ -10,7 +10,6 @@ export type EmbedDocumentCompletedPageProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const EmbedDocumentCompleted = ({ name, signature }: EmbedDocumentCompletedPageProps) => {
|
export const EmbedDocumentCompleted = ({ name, signature }: EmbedDocumentCompletedPageProps) => {
|
||||||
console.log({ signature });
|
|
||||||
return (
|
return (
|
||||||
<div className="embed--DocumentCompleted 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">
|
<h3 className="text-foreground text-2xl font-semibold">
|
||||||
|
|||||||
@ -13,10 +13,6 @@ import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'
|
|||||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
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 { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import type { DocumentMeta, Recipient, Signature, TemplateMeta } from '@documenso/prisma/client';
|
import type { DocumentMeta, Recipient, Signature, TemplateMeta } from '@documenso/prisma/client';
|
||||||
import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client';
|
import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client';
|
||||||
@ -53,7 +49,7 @@ export type EmbedDirectTemplateClientPageProps = {
|
|||||||
fields: Field[];
|
fields: Field[];
|
||||||
metadata?: DocumentMeta | TemplateMeta | null;
|
metadata?: DocumentMeta | TemplateMeta | null;
|
||||||
hidePoweredBy?: boolean;
|
hidePoweredBy?: boolean;
|
||||||
allowWhiteLabelling?: boolean;
|
isPlatformOrEnterprise?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EmbedDirectTemplateClientPage = ({
|
export const EmbedDirectTemplateClientPage = ({
|
||||||
@ -64,7 +60,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
fields,
|
fields,
|
||||||
metadata,
|
metadata,
|
||||||
hidePoweredBy = false,
|
hidePoweredBy = false,
|
||||||
allowWhiteLabelling = false,
|
isPlatformOrEnterprise = false,
|
||||||
}: EmbedDirectTemplateClientPageProps) => {
|
}: EmbedDirectTemplateClientPageProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@ -98,7 +94,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(() => fields);
|
const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(() => fields);
|
||||||
|
|
||||||
const [pendingFields, _completedFields] = [
|
const [pendingFields, _completedFields] = [
|
||||||
localFields.filter((field) => isFieldUnsignedAndRequired(field)),
|
localFields.filter((field) => !field.inserted),
|
||||||
localFields.filter((field) => field.inserted),
|
localFields.filter((field) => field.inserted),
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -116,7 +112,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
|
|
||||||
const newField: DirectTemplateLocalField = structuredClone({
|
const newField: DirectTemplateLocalField = structuredClone({
|
||||||
...field,
|
...field,
|
||||||
customText: payload.value ?? '',
|
customText: payload.value,
|
||||||
inserted: true,
|
inserted: true,
|
||||||
signedValue: payload,
|
signedValue: payload,
|
||||||
});
|
});
|
||||||
@ -127,10 +123,8 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
created: new Date(),
|
created: new Date(),
|
||||||
recipientId: 1,
|
recipientId: 1,
|
||||||
fieldId: 1,
|
fieldId: 1,
|
||||||
signatureImageAsBase64:
|
signatureImageAsBase64: payload.value.startsWith('data:') ? payload.value : null,
|
||||||
payload.value && payload.value.startsWith('data:') ? payload.value : null,
|
typedSignature: payload.value.startsWith('data:') ? null : payload.value,
|
||||||
typedSignature:
|
|
||||||
payload.value && !payload.value.startsWith('data:') ? payload.value : null,
|
|
||||||
} satisfies Signature;
|
} satisfies Signature;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,7 +182,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onNextFieldClick = () => {
|
const onNextFieldClick = () => {
|
||||||
validateFieldsInserted(pendingFields);
|
validateFieldsInserted(localFields);
|
||||||
|
|
||||||
setShowPendingFieldTooltip(true);
|
setShowPendingFieldTooltip(true);
|
||||||
setIsExpanded(false);
|
setIsExpanded(false);
|
||||||
@ -200,7 +194,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const valid = validateFieldsInserted(pendingFields);
|
const valid = validateFieldsInserted(localFields);
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
setShowPendingFieldTooltip(true);
|
setShowPendingFieldTooltip(true);
|
||||||
@ -213,6 +207,12 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
directTemplateExternalId = decodeURIComponent(directTemplateExternalId);
|
directTemplateExternalId = decodeURIComponent(directTemplateExternalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
localFields.forEach((field) => {
|
||||||
|
if (!field.signedValue) {
|
||||||
|
throw new Error('Invalid configuration');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
documentId,
|
documentId,
|
||||||
token: documentToken,
|
token: documentToken,
|
||||||
@ -223,11 +223,13 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
directRecipientName: fullName,
|
directRecipientName: fullName,
|
||||||
directRecipientEmail: email,
|
directRecipientEmail: email,
|
||||||
templateUpdatedAt: updatedAt,
|
templateUpdatedAt: updatedAt,
|
||||||
signedFieldValues: localFields
|
signedFieldValues: localFields.map((field) => {
|
||||||
.filter((field) => {
|
if (!field.signedValue) {
|
||||||
return field.signedValue && (isRequiredField(field) || field.inserted);
|
throw new Error('Invalid configuration');
|
||||||
})
|
}
|
||||||
.map((field) => field.signedValue!),
|
|
||||||
|
return field.signedValue;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (window.parent) {
|
if (window.parent) {
|
||||||
@ -286,7 +288,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
document.documentElement.classList.add('dark-mode-disabled');
|
document.documentElement.classList.add('dark-mode-disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowWhiteLabelling) {
|
if (isPlatformOrEnterprise) {
|
||||||
injectCss({
|
injectCss({
|
||||||
css: data.css,
|
css: data.css,
|
||||||
cssVars: data.cssVars,
|
cssVars: data.cssVars,
|
||||||
@ -347,7 +349,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
{/* Widget */}
|
{/* Widget */}
|
||||||
<div
|
<div
|
||||||
key={isExpanded ? 'expanded' : 'collapsed'}
|
key={isExpanded ? 'expanded' : 'collapsed'}
|
||||||
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"
|
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}
|
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">
|
<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">
|
||||||
@ -358,34 +360,19 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
<Trans>Sign document</Trans>
|
<Trans>Sign document</Trans>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{isExpanded ? (
|
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
||||||
<Button
|
{isExpanded ? (
|
||||||
variant="outline"
|
<LucideChevronDown
|
||||||
className="h-8 w-8 p-0 md:hidden"
|
className="text-muted-foreground h-5 w-5"
|
||||||
onClick={() => setIsExpanded(false)}
|
onClick={() => setIsExpanded(false)}
|
||||||
>
|
/>
|
||||||
<LucideChevronDown className="text-muted-foreground h-5 w-5" />
|
) : (
|
||||||
</Button>
|
<LucideChevronUp
|
||||||
) : pendingFields.length > 0 ? (
|
className="text-muted-foreground h-5 w-5"
|
||||||
<Button
|
onClick={() => setIsExpanded(true)}
|
||||||
variant="outline"
|
/>
|
||||||
className="h-8 w-8 p-0 md:hidden"
|
)}
|
||||||
onClick={() => setIsExpanded(true)}
|
</Button>
|
||||||
>
|
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -430,42 +417,40 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasSignatureField && (
|
<div>
|
||||||
<div>
|
<Label htmlFor="Signature">
|
||||||
<Label htmlFor="Signature">
|
<Trans>Signature</Trans>
|
||||||
<Trans>Signature</Trans>
|
</Label>
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Card className="mt-2" gradient degrees={-120}>
|
<Card className="mt-2" gradient degrees={-120}>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<SignaturePad
|
<SignaturePad
|
||||||
className="h-44 w-full"
|
className="h-44 w-full"
|
||||||
disabled={isThrottled || isSubmitting}
|
disabled={isThrottled || isSubmitting}
|
||||||
defaultValue={signature ?? undefined}
|
defaultValue={signature ?? undefined}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setSignature(value);
|
setSignature(value);
|
||||||
}}
|
}}
|
||||||
onValidityChange={(isValid) => {
|
onValidityChange={(isValid) => {
|
||||||
setSignatureValid(isValid);
|
setSignatureValid(isValid);
|
||||||
}}
|
}}
|
||||||
allowTypedSignature={Boolean(
|
allowTypedSignature={Boolean(
|
||||||
metadata &&
|
metadata &&
|
||||||
'typedSignatureEnabled' in metadata &&
|
'typedSignatureEnabled' in metadata &&
|
||||||
metadata.typedSignatureEnabled,
|
metadata.typedSignatureEnabled,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{hasSignatureField && !signatureValid && (
|
{hasSignatureField && !signatureValid && (
|
||||||
<div className="text-destructive mt-2 text-sm">
|
<div className="text-destructive mt-2 text-sm">
|
||||||
<Trans>
|
<Trans>
|
||||||
Signature is too small. Please provide a more complete signature.
|
Signature is too small. Please provide a more complete signature.
|
||||||
</Trans>
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { notFound } from 'next/navigation';
|
|||||||
|
|
||||||
import { match } from 'ts-pattern';
|
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 { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
||||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||||
@ -56,16 +55,12 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem
|
|||||||
documentAuth: template.authOptions,
|
documentAuth: template.authOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isPlatformDocument, isEnterpriseDocument, isCommunityPlan] = await Promise.all([
|
const [isPlatformDocument, isEnterpriseDocument] = await Promise.all([
|
||||||
isDocumentPlatform(template),
|
isDocumentPlatform(template),
|
||||||
isUserEnterprise({
|
isUserEnterprise({
|
||||||
userId: template.userId,
|
userId: template.userId,
|
||||||
teamId: template.teamId ?? undefined,
|
teamId: template.teamId ?? undefined,
|
||||||
}),
|
}),
|
||||||
isUserCommunityPlan({
|
|
||||||
userId: template.userId,
|
|
||||||
teamId: template.teamId ?? undefined,
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
||||||
@ -110,10 +105,8 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem
|
|||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
metadata={template.templateMeta}
|
metadata={template.templateMeta}
|
||||||
hidePoweredBy={
|
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
|
||||||
isCommunityPlan || isPlatformDocument || isEnterpriseDocument || hidePoweredBy
|
isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument}
|
||||||
}
|
|
||||||
allowWhiteLabelling={isCommunityPlan || isPlatformDocument || isEnterpriseDocument}
|
|
||||||
/>
|
/>
|
||||||
</RecipientProvider>
|
</RecipientProvider>
|
||||||
</DocumentAuthProvider>
|
</DocumentAuthProvider>
|
||||||
|
|||||||
@ -1,40 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useId, useLayoutEffect, useMemo, useState } from 'react';
|
import { useEffect, useId, useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans, msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
@ -8,16 +8,9 @@ import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
|||||||
|
|
||||||
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
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 { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client';
|
import type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client';
|
||||||
import {
|
import { type DocumentData, type Field, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||||
type DocumentData,
|
|
||||||
type Field,
|
|
||||||
FieldType,
|
|
||||||
RecipientRole,
|
|
||||||
SigningStatus,
|
|
||||||
} from '@documenso/prisma/client';
|
|
||||||
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||||
@ -33,13 +26,11 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
||||||
import { RecipientProvider } from '~/app/(signing)/sign/[token]/recipient-context';
|
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 { Logo } from '~/components/branding/logo';
|
||||||
|
|
||||||
import { EmbedClientLoading } from '../../client-loading';
|
import { EmbedClientLoading } from '../../client-loading';
|
||||||
import { EmbedDocumentCompleted } from '../../completed';
|
import { EmbedDocumentCompleted } from '../../completed';
|
||||||
import { EmbedDocumentFields } from '../../document-fields';
|
import { EmbedDocumentFields } from '../../document-fields';
|
||||||
import { EmbedDocumentRejected } from '../../rejected';
|
|
||||||
import { injectCss } from '../../util';
|
import { injectCss } from '../../util';
|
||||||
import { ZSignDocumentEmbedDataSchema } from './schema';
|
import { ZSignDocumentEmbedDataSchema } from './schema';
|
||||||
|
|
||||||
@ -52,7 +43,7 @@ export type EmbedSignDocumentClientPageProps = {
|
|||||||
metadata?: DocumentMeta | TemplateMeta | null;
|
metadata?: DocumentMeta | TemplateMeta | null;
|
||||||
isCompleted?: boolean;
|
isCompleted?: boolean;
|
||||||
hidePoweredBy?: boolean;
|
hidePoweredBy?: boolean;
|
||||||
allowWhitelabelling?: boolean;
|
isPlatformOrEnterprise?: boolean;
|
||||||
allRecipients?: RecipientWithFields[];
|
allRecipients?: RecipientWithFields[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -65,7 +56,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
metadata,
|
metadata,
|
||||||
isCompleted,
|
isCompleted,
|
||||||
hidePoweredBy = false,
|
hidePoweredBy = false,
|
||||||
allowWhitelabelling = false,
|
isPlatformOrEnterprise = false,
|
||||||
allRecipients = [],
|
allRecipients = [],
|
||||||
}: EmbedSignDocumentClientPageProps) => {
|
}: EmbedSignDocumentClientPageProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -84,9 +75,6 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||||
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
||||||
const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
|
const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
|
||||||
const [hasRejectedDocument, setHasRejectedDocument] = useState(
|
|
||||||
recipient.signingStatus === SigningStatus.REJECTED,
|
|
||||||
);
|
|
||||||
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(
|
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(
|
||||||
allRecipients.length > 0 ? allRecipients[0].id : null,
|
allRecipients.length > 0 ? allRecipients[0].id : null,
|
||||||
);
|
);
|
||||||
@ -95,34 +83,25 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
const [isNameLocked, setIsNameLocked] = useState(false);
|
const [isNameLocked, setIsNameLocked] = useState(false);
|
||||||
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
||||||
|
|
||||||
const [allowDocumentRejection, setAllowDocumentRejection] = useState(false);
|
|
||||||
|
|
||||||
const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId);
|
const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId);
|
||||||
const isAssistantMode = recipient.role === RecipientRole.ASSISTANT;
|
const isAssistantMode = recipient.role === RecipientRole.ASSISTANT;
|
||||||
|
|
||||||
const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500);
|
const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500);
|
||||||
|
|
||||||
const [pendingFields, _completedFields] = [
|
const [pendingFields, _completedFields] = [
|
||||||
fields.filter(
|
fields.filter((field) => field.recipientId === recipient.id && !field.inserted),
|
||||||
(field) => field.recipientId === recipient.id && isFieldUnsignedAndRequired(field),
|
|
||||||
),
|
|
||||||
fields.filter((field) => field.inserted),
|
fields.filter((field) => field.inserted),
|
||||||
];
|
];
|
||||||
|
|
||||||
const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } =
|
const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } =
|
||||||
trpc.recipient.completeDocumentWithToken.useMutation();
|
trpc.recipient.completeDocumentWithToken.useMutation();
|
||||||
|
|
||||||
const fieldsRequiringValidation = useMemo(
|
|
||||||
() => fields.filter(isFieldUnsignedAndRequired),
|
|
||||||
[fields],
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
|
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
|
||||||
|
|
||||||
const assistantSignersId = useId();
|
const assistantSignersId = useId();
|
||||||
|
|
||||||
const onNextFieldClick = () => {
|
const onNextFieldClick = () => {
|
||||||
validateFieldsInserted(fieldsRequiringValidation);
|
validateFieldsInserted(fields);
|
||||||
|
|
||||||
setShowPendingFieldTooltip(true);
|
setShowPendingFieldTooltip(true);
|
||||||
setIsExpanded(false);
|
setIsExpanded(false);
|
||||||
@ -134,7 +113,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const valid = validateFieldsInserted(fieldsRequiringValidation);
|
const valid = validateFieldsInserted(fields);
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
setShowPendingFieldTooltip(true);
|
setShowPendingFieldTooltip(true);
|
||||||
@ -182,25 +161,6 @@ 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(() => {
|
useLayoutEffect(() => {
|
||||||
const hash = window.location.hash.slice(1);
|
const hash = window.location.hash.slice(1);
|
||||||
|
|
||||||
@ -214,13 +174,12 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
// Since a recipient can be provided a name we can lock it without requiring
|
// 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.
|
// a to be provided by the parent application, unlike direct templates.
|
||||||
setIsNameLocked(!!data.lockName);
|
setIsNameLocked(!!data.lockName);
|
||||||
setAllowDocumentRejection(!!data.allowDocumentRejection);
|
|
||||||
|
|
||||||
if (data.darkModeDisabled) {
|
if (data.darkModeDisabled) {
|
||||||
document.documentElement.classList.add('dark-mode-disabled');
|
document.documentElement.classList.add('dark-mode-disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowWhitelabelling) {
|
if (isPlatformOrEnterprise) {
|
||||||
injectCss({
|
injectCss({
|
||||||
css: data.css,
|
css: data.css,
|
||||||
cssVars: data.cssVars,
|
cssVars: data.cssVars,
|
||||||
@ -249,10 +208,6 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
}
|
}
|
||||||
}, [hasFinishedInit, hasDocumentLoaded]);
|
}, [hasFinishedInit, hasDocumentLoaded]);
|
||||||
|
|
||||||
if (hasRejectedDocument) {
|
|
||||||
return <EmbedDocumentRejected name={fullName} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasCompletedDocument) {
|
if (hasCompletedDocument) {
|
||||||
return (
|
return (
|
||||||
<EmbedDocumentCompleted
|
<EmbedDocumentCompleted
|
||||||
@ -274,16 +229,6 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
<div className="embed--Root relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
<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 />}
|
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
|
||||||
|
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="embed--DocumentContainer relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
|
<div className="embed--DocumentContainer relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
|
||||||
{/* Viewer */}
|
{/* Viewer */}
|
||||||
<div className="embed--DocumentViewer flex-1">
|
<div className="embed--DocumentViewer flex-1">
|
||||||
@ -296,7 +241,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
{/* Widget */}
|
{/* Widget */}
|
||||||
<div
|
<div
|
||||||
key={isExpanded ? 'expanded' : 'collapsed'}
|
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"
|
className="embed--DocumentWidgetContainer 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}
|
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">
|
<div className="embed--DocumentWidget border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
|
||||||
@ -311,36 +256,19 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{isExpanded ? (
|
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
||||||
<Button
|
{isExpanded ? (
|
||||||
variant="outline"
|
<LucideChevronDown
|
||||||
className="bg-background dark:bg-foreground h-8 w-8 p-0 md:hidden"
|
className="text-muted-foreground h-5 w-5"
|
||||||
onClick={() => setIsExpanded(false)}
|
onClick={() => setIsExpanded(false)}
|
||||||
>
|
/>
|
||||||
<LucideChevronDown className="text-muted-foreground dark:text-background h-5 w-5" />
|
) : (
|
||||||
</Button>
|
<LucideChevronUp
|
||||||
) : pendingFields.length > 0 ? (
|
className="text-muted-foreground h-5 w-5"
|
||||||
<Button
|
onClick={() => setIsExpanded(true)}
|
||||||
variant="outline"
|
/>
|
||||||
className="bg-background dark:bg-foreground h-8 w-8 p-0 md:hidden"
|
)}
|
||||||
onClick={() => setIsExpanded(true)}
|
</Button>
|
||||||
>
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -444,42 +372,40 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasSignatureField && (
|
<div>
|
||||||
<div>
|
<Label htmlFor="Signature">
|
||||||
<Label htmlFor="Signature">
|
<Trans>Signature</Trans>
|
||||||
<Trans>Signature</Trans>
|
</Label>
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Card className="mt-2" gradient degrees={-120}>
|
<Card className="mt-2" gradient degrees={-120}>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<SignaturePad
|
<SignaturePad
|
||||||
className="h-44 w-full"
|
className="h-44 w-full"
|
||||||
disabled={isThrottled || isSubmitting}
|
disabled={isThrottled || isSubmitting}
|
||||||
defaultValue={signature ?? undefined}
|
defaultValue={signature ?? undefined}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setSignature(value);
|
setSignature(value);
|
||||||
}}
|
}}
|
||||||
onValidityChange={(isValid) => {
|
onValidityChange={(isValid) => {
|
||||||
setSignatureValid(isValid);
|
setSignatureValid(isValid);
|
||||||
}}
|
}}
|
||||||
allowTypedSignature={Boolean(
|
allowTypedSignature={Boolean(
|
||||||
metadata &&
|
metadata &&
|
||||||
'typedSignatureEnabled' in metadata &&
|
'typedSignatureEnabled' in metadata &&
|
||||||
metadata.typedSignatureEnabled,
|
metadata.typedSignatureEnabled,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{hasSignatureField && !signatureValid && (
|
{hasSignatureField && !signatureValid && (
|
||||||
<div className="text-destructive mt-2 text-sm">
|
<div className="text-destructive mt-2 text-sm">
|
||||||
<Trans>
|
<Trans>
|
||||||
Signature is too small. Please provide a more complete signature.
|
Signature is too small. Please provide a more complete signature.
|
||||||
</Trans>
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -494,7 +420,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
className={allowDocumentRejection ? 'col-start-2' : 'col-span-2'}
|
className="col-start-2"
|
||||||
disabled={
|
disabled={
|
||||||
isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
|
isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { notFound } from 'next/navigation';
|
|||||||
|
|
||||||
import { match } from 'ts-pattern';
|
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 { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
||||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||||
@ -63,16 +62,12 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
|
|||||||
return <EmbedPaywall />;
|
return <EmbedPaywall />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [isPlatformDocument, isEnterpriseDocument, isCommunityPlan] = await Promise.all([
|
const [isPlatformDocument, isEnterpriseDocument] = await Promise.all([
|
||||||
isDocumentPlatform(document),
|
isDocumentPlatform(document),
|
||||||
isUserEnterprise({
|
isUserEnterprise({
|
||||||
userId: document.userId,
|
userId: document.userId,
|
||||||
teamId: document.teamId ?? undefined,
|
teamId: document.teamId ?? undefined,
|
||||||
}),
|
}),
|
||||||
isUserCommunityPlan({
|
|
||||||
userId: document.userId,
|
|
||||||
teamId: document.teamId ?? undefined,
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||||
@ -131,10 +126,8 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
|
|||||||
fields={fields}
|
fields={fields}
|
||||||
metadata={document.documentMeta}
|
metadata={document.documentMeta}
|
||||||
isCompleted={document.status === DocumentStatus.COMPLETED}
|
isCompleted={document.status === DocumentStatus.COMPLETED}
|
||||||
hidePoweredBy={
|
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
|
||||||
isCommunityPlan || isPlatformDocument || isEnterpriseDocument || hidePoweredBy
|
isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument}
|
||||||
}
|
|
||||||
allowWhitelabelling={isCommunityPlan || isPlatformDocument || isEnterpriseDocument}
|
|
||||||
allRecipients={allRecipients}
|
allRecipients={allRecipients}
|
||||||
/>
|
/>
|
||||||
</DocumentAuthProvider>
|
</DocumentAuthProvider>
|
||||||
|
|||||||
@ -13,5 +13,4 @@ export const ZSignDocumentEmbedDataSchema = ZBaseEmbedDataSchema.extend({
|
|||||||
.optional()
|
.optional()
|
||||||
.transform((value) => value || undefined),
|
.transform((value) => value || undefined),
|
||||||
lockName: z.boolean().optional().default(false),
|
lockName: z.boolean().optional().default(false),
|
||||||
allowDocumentRejection: z.boolean().optional(),
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -73,7 +73,7 @@ export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: Deskto
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-muted-foreground flex w-full max-w-96 items-center justify-between rounded-lg"
|
className="text-muted-foreground flex w-96 items-center justify-between rounded-lg"
|
||||||
onClick={() => setIsCommandMenuOpen(true)}
|
onClick={() => setIsCommandMenuOpen(true)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@ -82,7 +82,7 @@ export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: Deskto
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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
|
{modifierKey}+K
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -363,40 +363,6 @@ export const DocumentHistorySheet = ({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(
|
|
||||||
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_CERTIFICATE_UPDATED },
|
|
||||||
({ data }) => (
|
|
||||||
<DocumentHistorySheetChanges
|
|
||||||
values={[
|
|
||||||
{
|
|
||||||
key: 'Old',
|
|
||||||
value: data.from,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'New',
|
|
||||||
value: data.to,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.with(
|
|
||||||
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_AUDIT_TRAIL_UPDATED },
|
|
||||||
({ data }) => (
|
|
||||||
<DocumentHistorySheetChanges
|
|
||||||
values={[
|
|
||||||
{
|
|
||||||
key: 'Old',
|
|
||||||
value: data.from,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'New',
|
|
||||||
value: data.to,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.exhaustive()}
|
.exhaustive()}
|
||||||
|
|
||||||
{isUserDetailsVisible && (
|
{isUserDetailsVisible && (
|
||||||
|
|||||||
21
package-lock.json
generated
21
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "1.9.1-rc.9",
|
"version": "1.9.1-rc.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "1.9.1-rc.9",
|
"version": "1.9.1-rc.1",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
@ -106,7 +106,7 @@
|
|||||||
},
|
},
|
||||||
"apps/web": {
|
"apps/web": {
|
||||||
"name": "@documenso/web",
|
"name": "@documenso/web",
|
||||||
"version": "1.9.1-rc.9",
|
"version": "1.9.1-rc.1",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/api": "*",
|
"@documenso/api": "*",
|
||||||
@ -35722,6 +35722,21 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"packages/trpc/node_modules/@next/swc-win32-ia32-msvc": {
|
||||||
|
"version": "14.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.6.tgz",
|
||||||
|
"integrity": "sha512-hNukAxq7hu4o5/UjPp5jqoBEtrpCbOmnUqZSKNJG8GrUVzfq0ucdhQFVrHcLRMvQcwqqDh1a5AJN9ORnNDpgBQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.9.1-rc.9",
|
"version": "1.9.1-rc.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"build:web": "turbo run build --filter=@documenso/web",
|
"build:web": "turbo run build --filter=@documenso/web",
|
||||||
|
|||||||
@ -586,7 +586,6 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
recipients: body.recipients,
|
recipients: body.recipients,
|
||||||
prefillFields: body.prefillFields,
|
|
||||||
override: {
|
override: {
|
||||||
title: body.title,
|
title: body.title,
|
||||||
...body.meta,
|
...body.meta,
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
ZRecipientActionAuthTypesSchema,
|
ZRecipientActionAuthTypesSchema,
|
||||||
} from '@documenso/lib/types/document-auth';
|
} from '@documenso/lib/types/document-auth';
|
||||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||||
import { ZFieldMetaPrefillFieldsSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
import {
|
import {
|
||||||
DocumentDataType,
|
DocumentDataType,
|
||||||
DocumentDistributionMethod,
|
DocumentDistributionMethod,
|
||||||
@ -299,7 +299,6 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
|
|||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).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<
|
export type TGenerateDocumentFromTemplateMutationSchema = z.infer<
|
||||||
|
|||||||
@ -1,612 +0,0 @@
|
|||||||
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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,600 +0,0 @@
|
|||||||
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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -384,9 +384,7 @@ test('[DOCUMENT_FLOW]: should be able to approve a document', async ({ page }) =
|
|||||||
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
|
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
await page
|
await page.getByRole('button', { name: 'Complete' }).click();
|
||||||
.getByRole('button', { name: role === RecipientRole.SIGNER ? 'Complete' : 'Approve' })
|
|
||||||
.click();
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', { name: role === RecipientRole.SIGNER ? 'Sign' : 'Approve' })
|
.getByRole('button', { name: role === RecipientRole.SIGNER ? 'Sign' : 'Approve' })
|
||||||
.click();
|
.click();
|
||||||
@ -456,7 +454,7 @@ test('[DOCUMENT_FLOW]: should be able to create, send with redirect url, sign a
|
|||||||
const { status } = await getDocumentByToken(token);
|
const { status } = await getDocumentByToken(token);
|
||||||
expect(status).toBe(DocumentStatus.PENDING);
|
expect(status).toBe(DocumentStatus.PENDING);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Approve' }).click();
|
await page.getByRole('button', { name: 'Complete' }).click();
|
||||||
await expect(page.getByRole('dialog').getByText('Complete Approval').first()).toBeVisible();
|
await expect(page.getByRole('dialog').getByText('Complete Approval').first()).toBeVisible();
|
||||||
await page.getByRole('button', { name: 'Approve' }).click();
|
await page.getByRole('button', { name: 'Approve' }).click();
|
||||||
|
|
||||||
|
|||||||
@ -4,14 +4,15 @@ import { stripe } from '@documenso/lib/server-only/stripe';
|
|||||||
type PlanType = (typeof STRIPE_PLAN_TYPE)[keyof typeof STRIPE_PLAN_TYPE];
|
type PlanType = (typeof STRIPE_PLAN_TYPE)[keyof typeof STRIPE_PLAN_TYPE];
|
||||||
|
|
||||||
export const getPricesByPlan = async (plan: PlanType | PlanType[]) => {
|
export const getPricesByPlan = async (plan: PlanType | PlanType[]) => {
|
||||||
const planTypes: string[] = typeof plan === 'string' ? [plan] : plan;
|
const planTypes = typeof plan === 'string' ? [plan] : plan;
|
||||||
|
|
||||||
const prices = await stripe.prices.list({
|
const query = planTypes.map((planType) => `metadata['plan']:'${planType}'`).join(' OR ');
|
||||||
|
|
||||||
|
const { data: prices } = await stripe.prices.search({
|
||||||
|
query,
|
||||||
expand: ['data.product'],
|
expand: ['data.product'],
|
||||||
limit: 100,
|
limit: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
return prices.data.filter(
|
return prices.filter((price) => price.type === 'recurring');
|
||||||
(price) => price.type === 'recurring' && planTypes.includes(price.metadata.plan),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,56 +0,0 @@
|
|||||||
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);
|
|
||||||
};
|
|
||||||
@ -17,7 +17,6 @@ const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
|||||||
documentLanguage: z.string(),
|
documentLanguage: z.string(),
|
||||||
includeSenderDetails: z.boolean(),
|
includeSenderDetails: z.boolean(),
|
||||||
includeSigningCertificate: z.boolean(),
|
includeSigningCertificate: z.boolean(),
|
||||||
includeAuditTrailLog: z.boolean(),
|
|
||||||
brandingEnabled: z.boolean(),
|
brandingEnabled: z.boolean(),
|
||||||
brandingLogo: z.string(),
|
brandingLogo: z.string(),
|
||||||
brandingUrl: z.string(),
|
brandingUrl: z.string(),
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import { signPdf } from '@documenso/signing';
|
|||||||
|
|
||||||
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
|
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
|
||||||
import PostHogServerClient from '../../../server-only/feature-flags/get-post-hog-server-client';
|
import PostHogServerClient from '../../../server-only/feature-flags/get-post-hog-server-client';
|
||||||
import { getAuditLogsPdf } from '../../../server-only/htmltopdf/get-audit-logs-pdf';
|
|
||||||
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
|
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
|
||||||
import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations';
|
import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations';
|
||||||
import { flattenForm } from '../../../server-only/pdf/flatten-form';
|
import { flattenForm } from '../../../server-only/pdf/flatten-form';
|
||||||
@ -58,7 +57,6 @@ export const run = async ({
|
|||||||
teamGlobalSettings: {
|
teamGlobalSettings: {
|
||||||
select: {
|
select: {
|
||||||
includeSigningCertificate: true,
|
includeSigningCertificate: true,
|
||||||
includeAuditTrailLog: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -123,36 +121,13 @@ export const run = async ({
|
|||||||
|
|
||||||
const pdfData = await getFile(documentData);
|
const pdfData = await getFile(documentData);
|
||||||
|
|
||||||
let includeSigningCertificate;
|
const certificateData =
|
||||||
|
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
|
||||||
if (document.teamId) {
|
? await getCertificatePdf({
|
||||||
includeSigningCertificate =
|
documentId,
|
||||||
document.team?.teamGlobalSettings?.includeSigningCertificate ?? true;
|
language: document.documentMeta?.language,
|
||||||
} else {
|
}).catch(() => null)
|
||||||
includeSigningCertificate = document.includeSigningCertificate ?? true;
|
: null;
|
||||||
}
|
|
||||||
|
|
||||||
const certificateData = includeSigningCertificate
|
|
||||||
? await getCertificatePdf({
|
|
||||||
documentId,
|
|
||||||
language: document.documentMeta?.language,
|
|
||||||
}).catch(() => null)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
let includeAuditTrailLog;
|
|
||||||
|
|
||||||
if (document.teamId) {
|
|
||||||
includeAuditTrailLog = document.team?.teamGlobalSettings?.includeAuditTrailLog ?? true;
|
|
||||||
} else {
|
|
||||||
includeAuditTrailLog = document.includeAuditTrailLog ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auditLogData = includeAuditTrailLog
|
|
||||||
? await getAuditLogsPdf({
|
|
||||||
documentId,
|
|
||||||
language: document.documentMeta?.language,
|
|
||||||
}).catch(() => null)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const newDataId = await io.runTask('decorate-and-sign-pdf', async () => {
|
const newDataId = await io.runTask('decorate-and-sign-pdf', async () => {
|
||||||
const pdfDoc = await PDFDocument.load(pdfData);
|
const pdfDoc = await PDFDocument.load(pdfData);
|
||||||
@ -175,16 +150,6 @@ export const run = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auditLogData) {
|
|
||||||
const auditLog = await PDFDocument.load(auditLogData);
|
|
||||||
|
|
||||||
const auditLogPages = await pdfDoc.copyPages(auditLog, auditLog.getPageIndices());
|
|
||||||
|
|
||||||
auditLogPages.forEach((page) => {
|
|
||||||
pdfDoc.addPage(page);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
if (field.inserted) {
|
if (field.inserted) {
|
||||||
await insertFieldInPDF(pdfDoc, field);
|
await insertFieldInPDF(pdfDoc, field);
|
||||||
|
|||||||
@ -28,6 +28,7 @@ export type CreateDocumentMetaOptions = {
|
|||||||
distributionMethod?: DocumentDistributionMethod;
|
distributionMethod?: DocumentDistributionMethod;
|
||||||
typedSignatureEnabled?: boolean;
|
typedSignatureEnabled?: boolean;
|
||||||
language?: SupportedLanguageCodes;
|
language?: SupportedLanguageCodes;
|
||||||
|
modifyNextSigner?: boolean;
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata: ApiRequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -46,6 +47,7 @@ export const upsertDocumentMeta = async ({
|
|||||||
distributionMethod,
|
distributionMethod,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
language,
|
language,
|
||||||
|
modifyNextSigner,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
}: CreateDocumentMetaOptions) => {
|
}: CreateDocumentMetaOptions) => {
|
||||||
const document = await prisma.document.findFirst({
|
const document = await prisma.document.findFirst({
|
||||||
@ -98,6 +100,7 @@ export const upsertDocumentMeta = async ({
|
|||||||
distributionMethod,
|
distributionMethod,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
language,
|
language,
|
||||||
|
modifyNextSigner,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
subject,
|
subject,
|
||||||
@ -111,6 +114,7 @@ export const upsertDocumentMeta = async ({
|
|||||||
distributionMethod,
|
distributionMethod,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
language,
|
language,
|
||||||
|
modifyNextSigner,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import {
|
|||||||
WebhookTriggerEvents,
|
WebhookTriggerEvents,
|
||||||
} from '@documenso/prisma/client';
|
} from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
||||||
import { jobs } from '../../jobs/client';
|
import { jobs } from '../../jobs/client';
|
||||||
import type { TRecipientActionAuth } from '../../types/document-auth';
|
import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||||
import {
|
import {
|
||||||
@ -29,6 +28,10 @@ export type CompleteDocumentWithTokenOptions = {
|
|||||||
userId?: number;
|
userId?: number;
|
||||||
authOptions?: TRecipientActionAuth;
|
authOptions?: TRecipientActionAuth;
|
||||||
requestMetadata?: RequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
|
nextSigner?: {
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptions) => {
|
const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptions) => {
|
||||||
@ -52,10 +55,53 @@ const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptio
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const delegateNextSigner = async ({
|
||||||
|
documentId,
|
||||||
|
currentRecipientId,
|
||||||
|
nextSigner,
|
||||||
|
}: {
|
||||||
|
documentId: number;
|
||||||
|
currentRecipientId: number;
|
||||||
|
nextSigner: { email: string; name: string };
|
||||||
|
}) => {
|
||||||
|
const document = await prisma.document.findUnique({
|
||||||
|
where: { id: documentId },
|
||||||
|
include: {
|
||||||
|
recipients: {
|
||||||
|
orderBy: [{ signingOrder: 'asc' }, { id: 'asc' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
throw new Error('Document not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentRecipient = document.recipients.find((r) => r.id === currentRecipientId);
|
||||||
|
const nextRecipient = document.recipients.find(
|
||||||
|
(r) => r.signingOrder === (currentRecipient?.signingOrder ?? 0) + 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!nextRecipient) {
|
||||||
|
throw new Error('Next recipient not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.recipient.update({
|
||||||
|
where: { id: nextRecipient.id },
|
||||||
|
data: {
|
||||||
|
email: nextSigner.email,
|
||||||
|
name: nextSigner.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return nextRecipient;
|
||||||
|
};
|
||||||
|
|
||||||
export const completeDocumentWithToken = async ({
|
export const completeDocumentWithToken = async ({
|
||||||
token,
|
token,
|
||||||
documentId,
|
documentId,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
|
nextSigner,
|
||||||
}: CompleteDocumentWithTokenOptions) => {
|
}: CompleteDocumentWithTokenOptions) => {
|
||||||
const document = await getDocument({ token, documentId });
|
const document = await getDocument({ token, documentId });
|
||||||
|
|
||||||
@ -73,13 +119,6 @@ export const completeDocumentWithToken = async ({
|
|||||||
throw new Error(`Recipient ${recipient.id} has already signed`);
|
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) {
|
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||||
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token });
|
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token });
|
||||||
|
|
||||||
@ -120,6 +159,18 @@ export const completeDocumentWithToken = async ({
|
|||||||
// throw new AppError(AppErrorCode.UNAUTHORIZED, 'Invalid authentication values');
|
// throw new AppError(AppErrorCode.UNAUTHORIZED, 'Invalid authentication values');
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
if (
|
||||||
|
nextSigner &&
|
||||||
|
document.documentMeta?.modifyNextSigner &&
|
||||||
|
document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL
|
||||||
|
) {
|
||||||
|
await delegateNextSigner({
|
||||||
|
documentId: document.id,
|
||||||
|
currentRecipientId: recipient.id,
|
||||||
|
nextSigner,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
await tx.recipient.update({
|
await tx.recipient.update({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@ -124,8 +124,6 @@ export const createDocument = async ({
|
|||||||
team?.teamGlobalSettings?.documentVisibility,
|
team?.teamGlobalSettings?.documentVisibility,
|
||||||
userTeamRole ?? TeamMemberRole.MEMBER,
|
userTeamRole ?? TeamMemberRole.MEMBER,
|
||||||
),
|
),
|
||||||
includeSigningCertificate: team?.teamGlobalSettings?.includeSigningCertificate ?? true,
|
|
||||||
includeAuditTrailLog: team?.teamGlobalSettings?.includeAuditTrailLog ?? true,
|
|
||||||
formValues,
|
formValues,
|
||||||
source: DocumentSource.DOCUMENT,
|
source: DocumentSource.DOCUMENT,
|
||||||
documentMeta: {
|
documentMeta: {
|
||||||
|
|||||||
@ -88,7 +88,6 @@ export const findDocuments = async ({
|
|||||||
const searchFilter: Prisma.DocumentWhereInput = {
|
const searchFilter: Prisma.DocumentWhereInput = {
|
||||||
OR: [
|
OR: [
|
||||||
{ title: { contains: query, mode: 'insensitive' } },
|
{ title: { contains: query, mode: 'insensitive' } },
|
||||||
{ externalId: { contains: query, mode: 'insensitive' } },
|
|
||||||
{ recipients: { some: { name: { contains: query, mode: 'insensitive' } } } },
|
{ recipients: { some: { name: { contains: query, mode: 'insensitive' } } } },
|
||||||
{ recipients: { some: { email: { contains: query, mode: 'insensitive' } } } },
|
{ recipients: { some: { email: { contains: query, mode: 'insensitive' } } } },
|
||||||
],
|
],
|
||||||
|
|||||||
@ -22,7 +22,6 @@ import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
|||||||
import { getFile } from '../../universal/upload/get-file';
|
import { getFile } from '../../universal/upload/get-file';
|
||||||
import { putPdfFile } from '../../universal/upload/put-file';
|
import { putPdfFile } from '../../universal/upload/put-file';
|
||||||
import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers';
|
import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers';
|
||||||
import { getAuditLogsPdf } from '../htmltopdf/get-audit-logs-pdf';
|
|
||||||
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
|
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
|
||||||
import { flattenAnnotations } from '../pdf/flatten-annotations';
|
import { flattenAnnotations } from '../pdf/flatten-annotations';
|
||||||
import { flattenForm } from '../pdf/flatten-form';
|
import { flattenForm } from '../pdf/flatten-form';
|
||||||
@ -62,7 +61,6 @@ export const sealDocument = async ({
|
|||||||
teamGlobalSettings: {
|
teamGlobalSettings: {
|
||||||
select: {
|
select: {
|
||||||
includeSigningCertificate: true,
|
includeSigningCertificate: true,
|
||||||
includeAuditTrailLog: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -111,36 +109,13 @@ export const sealDocument = async ({
|
|||||||
// !: Need to write the fields onto the document as a hard copy
|
// !: Need to write the fields onto the document as a hard copy
|
||||||
const pdfData = await getFile(documentData);
|
const pdfData = await getFile(documentData);
|
||||||
|
|
||||||
let includeSigningCertificate;
|
const certificateData =
|
||||||
|
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
|
||||||
if (document.teamId) {
|
? await getCertificatePdf({
|
||||||
includeSigningCertificate =
|
documentId,
|
||||||
document.team?.teamGlobalSettings?.includeSigningCertificate ?? true;
|
language: document.documentMeta?.language,
|
||||||
} else {
|
}).catch(() => null)
|
||||||
includeSigningCertificate = document.includeSigningCertificate ?? true;
|
: null;
|
||||||
}
|
|
||||||
|
|
||||||
const certificateData = includeSigningCertificate
|
|
||||||
? await getCertificatePdf({
|
|
||||||
documentId,
|
|
||||||
language: document.documentMeta?.language,
|
|
||||||
}).catch(() => null)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
let includeAuditTrailLog;
|
|
||||||
|
|
||||||
if (document.teamId) {
|
|
||||||
includeAuditTrailLog = document.team?.teamGlobalSettings?.includeAuditTrailLog ?? true;
|
|
||||||
} else {
|
|
||||||
includeAuditTrailLog = document.includeAuditTrailLog ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auditLogData = includeAuditTrailLog
|
|
||||||
? await getAuditLogsPdf({
|
|
||||||
documentId,
|
|
||||||
language: document.documentMeta?.language,
|
|
||||||
}).catch(() => null)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const doc = await PDFDocument.load(pdfData);
|
const doc = await PDFDocument.load(pdfData);
|
||||||
|
|
||||||
@ -159,16 +134,6 @@ export const sealDocument = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auditLogData) {
|
|
||||||
const auditLog = await PDFDocument.load(auditLogData);
|
|
||||||
|
|
||||||
const auditLogPages = await doc.copyPages(auditLog, auditLog.getPageIndices());
|
|
||||||
|
|
||||||
auditLogPages.forEach((page) => {
|
|
||||||
doc.addPage(page);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
await insertFieldInPDF(doc, field);
|
await insertFieldInPDF(doc, field);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,14 +34,6 @@ export const searchDocumentsWithKeyword = async ({
|
|||||||
userId: userId,
|
userId: userId,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
externalId: {
|
|
||||||
contains: query,
|
|
||||||
mode: 'insensitive',
|
|
||||||
},
|
|
||||||
userId: userId,
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
recipients: {
|
recipients: {
|
||||||
some: {
|
some: {
|
||||||
@ -96,23 +88,6 @@ export const searchDocumentsWithKeyword = async ({
|
|||||||
},
|
},
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
externalId: {
|
|
||||||
contains: query,
|
|
||||||
mode: 'insensitive',
|
|
||||||
},
|
|
||||||
teamId: {
|
|
||||||
not: null,
|
|
||||||
},
|
|
||||||
team: {
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
userId: userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
|
|||||||
@ -21,8 +21,6 @@ export type UpdateDocumentOptions = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
externalId?: string | null;
|
externalId?: string | null;
|
||||||
visibility?: DocumentVisibility | null;
|
visibility?: DocumentVisibility | null;
|
||||||
includeSigningCertificate?: boolean;
|
|
||||||
includeAuditTrailLog?: boolean;
|
|
||||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
||||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
globalActionAuth?: TDocumentActionAuthTypes | null;
|
||||||
};
|
};
|
||||||
@ -158,12 +156,6 @@ export const updateDocument = async ({
|
|||||||
documentGlobalActionAuth === undefined || documentGlobalActionAuth === newGlobalActionAuth;
|
documentGlobalActionAuth === undefined || documentGlobalActionAuth === newGlobalActionAuth;
|
||||||
const isDocumentVisibilitySame =
|
const isDocumentVisibilitySame =
|
||||||
data.visibility === undefined || data.visibility === document.visibility;
|
data.visibility === undefined || data.visibility === document.visibility;
|
||||||
const isIncludeSigningCertificateSame =
|
|
||||||
data.includeSigningCertificate === undefined ||
|
|
||||||
data.includeSigningCertificate === document.includeSigningCertificate;
|
|
||||||
const isIncludeAuditTrailLogSame =
|
|
||||||
data.includeAuditTrailLog === undefined ||
|
|
||||||
data.includeAuditTrailLog === document.includeAuditTrailLog;
|
|
||||||
|
|
||||||
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
|
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
|
||||||
|
|
||||||
@ -243,34 +235,6 @@ export const updateDocument = async ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isIncludeSigningCertificateSame) {
|
|
||||||
auditLogs.push(
|
|
||||||
createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_CERTIFICATE_UPDATED,
|
|
||||||
documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
from: String(document.includeSigningCertificate),
|
|
||||||
to: String(data.includeSigningCertificate || false),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isIncludeAuditTrailLogSame) {
|
|
||||||
auditLogs.push(
|
|
||||||
createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_AUDIT_TRAIL_UPDATED,
|
|
||||||
documentId,
|
|
||||||
metadata: requestMetadata,
|
|
||||||
data: {
|
|
||||||
from: String(document.includeAuditTrailLog),
|
|
||||||
to: String(data.includeAuditTrailLog || false),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Early return if nothing is required.
|
// Early return if nothing is required.
|
||||||
if (auditLogs.length === 0) {
|
if (auditLogs.length === 0) {
|
||||||
return document;
|
return document;
|
||||||
@ -290,8 +254,6 @@ export const updateDocument = async ({
|
|||||||
title: data.title,
|
title: data.title,
|
||||||
externalId: data.externalId,
|
externalId: data.externalId,
|
||||||
visibility: data.visibility as DocumentVisibility,
|
visibility: data.visibility as DocumentVisibility,
|
||||||
includeSigningCertificate: data.includeSigningCertificate,
|
|
||||||
includeAuditTrailLog: data.includeAuditTrailLog,
|
|
||||||
authOptions,
|
authOptions,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,74 +0,0 @@
|
|||||||
import { DateTime } from 'luxon';
|
|
||||||
import type { Browser } from 'playwright';
|
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|
||||||
import { type SupportedLanguageCodes, isValidLanguageCode } from '../../constants/i18n';
|
|
||||||
import { encryptSecondaryData } from '../crypto/encrypt';
|
|
||||||
|
|
||||||
export type GetAuditLogsPdfParams = {
|
|
||||||
documentId: number;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
language?: SupportedLanguageCodes | (string & {});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAuditLogsPdf = async ({ documentId, language }: GetAuditLogsPdfParams) => {
|
|
||||||
const { chromium } = await import('playwright');
|
|
||||||
|
|
||||||
const encryptedId = encryptSecondaryData({
|
|
||||||
data: documentId.toString(),
|
|
||||||
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let browser: Browser;
|
|
||||||
|
|
||||||
if (process.env.NEXT_PRIVATE_BROWSERLESS_URL) {
|
|
||||||
// !: Use CDP rather than the default `connect` method to avoid coupling to the playwright version.
|
|
||||||
// !: Previously we would have to keep the playwright version in sync with the browserless version to avoid errors.
|
|
||||||
browser = await chromium.connectOverCDP(process.env.NEXT_PRIVATE_BROWSERLESS_URL);
|
|
||||||
} else {
|
|
||||||
browser = await chromium.launch();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!browser) {
|
|
||||||
throw new Error(
|
|
||||||
'Failed to establish a browser, please ensure you have either a Browserless.io url or chromium browser installed',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const browserContext = await browser.newContext();
|
|
||||||
|
|
||||||
const page = await browserContext.newPage();
|
|
||||||
|
|
||||||
const lang = isValidLanguageCode(language) ? language : 'en';
|
|
||||||
|
|
||||||
await page.context().addCookies([
|
|
||||||
{
|
|
||||||
name: 'language',
|
|
||||||
value: lang,
|
|
||||||
url: NEXT_PUBLIC_WEBAPP_URL(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/audit-log?d=${encryptedId}`, {
|
|
||||||
waitUntil: 'networkidle',
|
|
||||||
timeout: 10_000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await page.pdf({
|
|
||||||
format: 'A4',
|
|
||||||
});
|
|
||||||
|
|
||||||
await browserContext.close();
|
|
||||||
|
|
||||||
void browser.close();
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
await browserContext.close();
|
|
||||||
|
|
||||||
void browser.close();
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
46
packages/lib/server-only/recipient/get-is-last-recipient.ts
Normal file
46
packages/lib/server-only/recipient/get-is-last-recipient.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { DocumentSigningOrder, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
export type GetIsLastRecipientOptions = {
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getIsLastRecipient({ token }: GetIsLastRecipientOptions) {
|
||||||
|
const document = await prisma.document.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
recipients: {
|
||||||
|
some: {
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
documentMeta: true,
|
||||||
|
recipients: {
|
||||||
|
where: {
|
||||||
|
role: {
|
||||||
|
not: RecipientRole.CC,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (document.documentMeta?.signingOrder !== DocumentSigningOrder.SEQUENTIAL) {
|
||||||
|
const unsignedRecipients = document.recipients.filter(
|
||||||
|
(recipient) => recipient.signingStatus !== SigningStatus.SIGNED,
|
||||||
|
);
|
||||||
|
|
||||||
|
return unsignedRecipients.length <= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { recipients } = document;
|
||||||
|
const currentRecipientIndex = recipients.findIndex((r) => r.token === token);
|
||||||
|
|
||||||
|
if (currentRecipientIndex === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentRecipientIndex === recipients.length - 1;
|
||||||
|
}
|
||||||
@ -17,7 +17,6 @@ export type UpdateTeamDocumentSettingsOptions = {
|
|||||||
includeSenderDetails: boolean;
|
includeSenderDetails: boolean;
|
||||||
typedSignatureEnabled: boolean;
|
typedSignatureEnabled: boolean;
|
||||||
includeSigningCertificate: boolean;
|
includeSigningCertificate: boolean;
|
||||||
includeAuditTrailLog: boolean;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -37,7 +36,6 @@ export const updateTeamDocumentSettings = async ({
|
|||||||
documentLanguage,
|
documentLanguage,
|
||||||
includeSenderDetails,
|
includeSenderDetails,
|
||||||
includeSigningCertificate,
|
includeSigningCertificate,
|
||||||
includeAuditTrailLog,
|
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
@ -63,7 +61,6 @@ export const updateTeamDocumentSettings = async ({
|
|||||||
includeSenderDetails,
|
includeSenderDetails,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
includeSigningCertificate,
|
includeSigningCertificate,
|
||||||
includeAuditTrailLog,
|
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
documentVisibility,
|
documentVisibility,
|
||||||
@ -71,7 +68,6 @@ export const updateTeamDocumentSettings = async ({
|
|||||||
includeSenderDetails,
|
includeSenderDetails,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
includeSigningCertificate,
|
includeSigningCertificate,
|
||||||
includeAuditTrailLog,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -37,7 +37,6 @@ import {
|
|||||||
mapDocumentToWebhookDocumentPayload,
|
mapDocumentToWebhookDocumentPayload,
|
||||||
} from '../../types/webhook-payload';
|
} from '../../types/webhook-payload';
|
||||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
import { isRequiredField } from '../../utils/advanced-fields-helpers';
|
|
||||||
import type { CreateDocumentAuditLogDataResponse } from '../../utils/document-audit-logs';
|
import type { CreateDocumentAuditLogDataResponse } from '../../utils/document-audit-logs';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
import {
|
import {
|
||||||
@ -176,28 +175,20 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
const metaSigningOrder = template.templateMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
|
const metaSigningOrder = template.templateMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
|
||||||
|
|
||||||
// Associate, validate and map to a query every direct template recipient field with the provided fields.
|
// 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(
|
const createDirectRecipientFieldArgs = await Promise.all(
|
||||||
fieldsToProcess.map(async (templateField) => {
|
directTemplateRecipient.fields.map(async (templateField) => {
|
||||||
const signedFieldValue = signedFieldValues.find(
|
const signedFieldValue = signedFieldValues.find(
|
||||||
(value) => value.fieldId === templateField.id,
|
(value) => value.fieldId === templateField.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isRequiredField(templateField) && !signedFieldValue) {
|
if (!signedFieldValue) {
|
||||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||||
message: 'Invalid, missing or changed fields',
|
message: 'Invalid, missing or changed fields',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (templateField.type === FieldType.NAME && directRecipientName === undefined) {
|
if (templateField.type === FieldType.NAME && directRecipientName === undefined) {
|
||||||
directRecipientName === signedFieldValue?.value;
|
directRecipientName === signedFieldValue.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const derivedRecipientActionAuth = await validateFieldAuth({
|
const derivedRecipientActionAuth = await validateFieldAuth({
|
||||||
@ -208,18 +199,9 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
},
|
},
|
||||||
field: templateField,
|
field: templateField,
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
authOptions: signedFieldValue?.authOptions,
|
authOptions: signedFieldValue.authOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!signedFieldValue) {
|
|
||||||
return {
|
|
||||||
templateField,
|
|
||||||
customText: '',
|
|
||||||
derivedRecipientActionAuth,
|
|
||||||
signature: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const { value, isBase64 } = signedFieldValue;
|
const { value, isBase64 } = signedFieldValue;
|
||||||
|
|
||||||
const isSignatureField =
|
const isSignatureField =
|
||||||
@ -397,7 +379,7 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
positionY: templateField.positionY,
|
positionY: templateField.positionY,
|
||||||
width: templateField.width,
|
width: templateField.width,
|
||||||
height: templateField.height,
|
height: templateField.height,
|
||||||
customText: customText ?? '',
|
customText,
|
||||||
inserted: true,
|
inserted: true,
|
||||||
fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
|
fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
|
||||||
})),
|
})),
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { match } from 'ts-pattern';
|
|
||||||
|
|
||||||
import { nanoid } from '@documenso/lib/universal/id';
|
import { nanoid } from '@documenso/lib/universal/id';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import type { DocumentDistributionMethod } from '@documenso/prisma/client';
|
import type { DocumentDistributionMethod } from '@documenso/prisma/client';
|
||||||
@ -19,20 +17,7 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
|
|||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||||
import { ZRecipientAuthOptionsSchema } from '../../types/document-auth';
|
import { ZRecipientAuthOptionsSchema } from '../../types/document-auth';
|
||||||
import type { TDocumentEmailSettings } from '../../types/document-email';
|
import type { TDocumentEmailSettings } from '../../types/document-email';
|
||||||
import type {
|
import { ZFieldMetaSchema } from '../../types/field-meta';
|
||||||
TCheckboxFieldMeta,
|
|
||||||
TDropdownFieldMeta,
|
|
||||||
TFieldMetaPrefillFieldsSchema,
|
|
||||||
TNumberFieldMeta,
|
|
||||||
TRadioFieldMeta,
|
|
||||||
TTextFieldMeta,
|
|
||||||
} from '../../types/field-meta';
|
|
||||||
import {
|
|
||||||
ZCheckboxFieldMeta,
|
|
||||||
ZDropdownFieldMeta,
|
|
||||||
ZFieldMetaSchema,
|
|
||||||
ZRadioFieldMeta,
|
|
||||||
} from '../../types/field-meta';
|
|
||||||
import {
|
import {
|
||||||
ZWebhookDocumentSchema,
|
ZWebhookDocumentSchema,
|
||||||
mapDocumentToWebhookDocumentPayload,
|
mapDocumentToWebhookDocumentPayload,
|
||||||
@ -65,7 +50,6 @@ export type CreateDocumentFromTemplateOptions = {
|
|||||||
email: string;
|
email: string;
|
||||||
signingOrder?: number | null;
|
signingOrder?: number | null;
|
||||||
}[];
|
}[];
|
||||||
prefillFields?: TFieldMetaPrefillFieldsSchema[];
|
|
||||||
customDocumentDataId?: string;
|
customDocumentDataId?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -88,165 +72,6 @@ export type CreateDocumentFromTemplateOptions = {
|
|||||||
requestMetadata: ApiRequestMetadata;
|
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 ({
|
export const createDocumentFromTemplate = async ({
|
||||||
templateId,
|
templateId,
|
||||||
externalId,
|
externalId,
|
||||||
@ -256,7 +81,6 @@ export const createDocumentFromTemplate = async ({
|
|||||||
customDocumentDataId,
|
customDocumentDataId,
|
||||||
override,
|
override,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
prefillFields,
|
|
||||||
}: CreateDocumentFromTemplateOptions) => {
|
}: CreateDocumentFromTemplateOptions) => {
|
||||||
const template = await prisma.template.findUnique({
|
const template = await prisma.template.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@ -434,47 +258,6 @@ export const createDocumentFromTemplate = async ({
|
|||||||
|
|
||||||
let fieldsToCreate: Omit<Field, 'id' | 'secondaryId' | 'templateId'>[] = [];
|
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 }) => {
|
Object.values(finalRecipients).forEach(({ email, fields }) => {
|
||||||
const recipient = document.recipients.find((recipient) => recipient.email === email);
|
const recipient = document.recipients.find((recipient) => recipient.email === email);
|
||||||
|
|
||||||
@ -483,25 +266,19 @@ export const createDocumentFromTemplate = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
fieldsToCreate = fieldsToCreate.concat(
|
fieldsToCreate = fieldsToCreate.concat(
|
||||||
fields.map((field) => {
|
fields.map((field) => ({
|
||||||
const prefillField = prefillFields?.find((value) => value.id === field.id);
|
documentId: document.id,
|
||||||
// Use type assertion to help TypeScript understand the structure
|
recipientId: recipient.id,
|
||||||
const updatedFieldMeta = getUpdatedFieldMeta(field, prefillField);
|
type: field.type,
|
||||||
|
page: field.page,
|
||||||
return {
|
positionX: field.positionX,
|
||||||
documentId: document.id,
|
positionY: field.positionY,
|
||||||
recipientId: recipient.id,
|
width: field.width,
|
||||||
type: field.type,
|
height: field.height,
|
||||||
page: field.page,
|
customText: '',
|
||||||
positionX: field.positionX,
|
inserted: false,
|
||||||
positionY: field.positionY,
|
fieldMeta: field.fieldMeta,
|
||||||
width: field.width,
|
})),
|
||||||
height: field.height,
|
|
||||||
customText: '',
|
|
||||||
inserted: false,
|
|
||||||
fieldMeta: updatedFieldMeta,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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
|
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:463
|
||||||
msgid "<0>Click to upload</0> or drag and drop"
|
msgid "<0>Click to upload</0> or drag and drop"
|
||||||
msgstr "<0>Cliquez pour importer</0> ou faites glisser et déposez"
|
msgstr "<0>Cliquez pour télécharger</0> ou faites glisser et déposez"
|
||||||
|
|
||||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx:287
|
#: 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."
|
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
|
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:108
|
||||||
msgid "An error occurred while uploading your document."
|
msgid "An error occurred while uploading your document."
|
||||||
msgstr "Une erreur est survenue lors de l'importation de votre document."
|
msgstr "Une erreur est survenue lors du téléchargement de votre document."
|
||||||
|
|
||||||
#: apps/web/src/app/(dashboard)/admin/documents/[id]/super-delete-document-dialog.tsx:58
|
#: 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
|
#: apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx:81
|
||||||
@ -1430,7 +1430,7 @@ msgstr "Cliquez ici pour réessayer"
|
|||||||
|
|
||||||
#: apps/web/src/components/(teams)/dialogs/invite-team-member-dialog.tsx:392
|
#: apps/web/src/components/(teams)/dialogs/invite-team-member-dialog.tsx:392
|
||||||
msgid "Click here to upload"
|
msgid "Click here to upload"
|
||||||
msgstr "Cliquez ici pour importer"
|
msgstr "Cliquez ici pour télécharger"
|
||||||
|
|
||||||
#: apps/web/src/components/(dashboard)/avatar/avatar-with-recipient.tsx:52
|
#: apps/web/src/components/(dashboard)/avatar/avatar-with-recipient.tsx:52
|
||||||
#: apps/web/src/components/(dashboard)/avatar/avatar-with-recipient.tsx:65
|
#: 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
|
#: 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."
|
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 importé. 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 téléchargé. 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
|
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:153
|
||||||
msgid "Controls the default visibility of an uploaded document."
|
msgid "Controls the default visibility of an uploaded document."
|
||||||
msgstr "Contrôle la visibilité par défaut d'un document importé."
|
msgstr "Contrôle la visibilité par défaut d'un document téléchargé."
|
||||||
|
|
||||||
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:232
|
#: 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."
|
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."
|
||||||
@ -2255,11 +2255,11 @@ msgstr "Document mis à jour"
|
|||||||
|
|
||||||
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:55
|
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:55
|
||||||
msgid "Document upload disabled due to unpaid invoices"
|
msgid "Document upload disabled due to unpaid invoices"
|
||||||
msgstr "Importation de documents désactivé en raison de factures impayées"
|
msgstr "Téléchargement du document désactivé en raison de factures impayées"
|
||||||
|
|
||||||
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:85
|
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:85
|
||||||
msgid "Document uploaded"
|
msgid "Document uploaded"
|
||||||
msgstr "Document importé"
|
msgstr "Document téléchargé"
|
||||||
|
|
||||||
#: apps/web/src/app/(signing)/sign/[token]/complete/page.tsx:131
|
#: apps/web/src/app/(signing)/sign/[token]/complete/page.tsx:131
|
||||||
msgid "Document Viewed"
|
msgid "Document Viewed"
|
||||||
@ -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
|
#: 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."
|
msgid "For each recipient, provide their email (required) and name (optional) in separate columns. Download the template CSV below for the correct format."
|
||||||
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."
|
msgstr "Pour chaque destinataire, fournissez leur email (obligatoire) et leur nom (facultatif) dans des colonnes séparées. Téléchargez le modèle CSV ci-dessous pour le format correct."
|
||||||
|
|
||||||
#: packages/lib/server-only/auth/send-forgot-password.ts:61
|
#: packages/lib/server-only/auth/send-forgot-password.ts:61
|
||||||
msgid "Forgot Password?"
|
msgid "Forgot Password?"
|
||||||
@ -2770,7 +2770,7 @@ msgstr "Authentification d'action de destinataire globale"
|
|||||||
#: apps/web/src/components/partials/not-found.tsx:67
|
#: apps/web/src/components/partials/not-found.tsx:67
|
||||||
#: packages/ui/primitives/document-flow/document-flow-root.tsx:142
|
#: packages/ui/primitives/document-flow/document-flow-root.tsx:142
|
||||||
msgid "Go Back"
|
msgid "Go Back"
|
||||||
msgstr "Retour"
|
msgstr "Retourner"
|
||||||
|
|
||||||
#: apps/web/src/app/(unauthenticated)/verify-email/[token]/client.tsx:48
|
#: apps/web/src/app/(unauthenticated)/verify-email/[token]/client.tsx:48
|
||||||
#: apps/web/src/app/(unauthenticated)/verify-email/[token]/page.tsx:73
|
#: 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
|
#: packages/email/templates/bulk-send-complete.tsx:36
|
||||||
msgid "Hi {userName},"
|
msgid "Hi {userName},"
|
||||||
msgstr "Bonjour {userName},"
|
msgstr "Bonjour, {userName},"
|
||||||
|
|
||||||
#: packages/email/templates/reset-password.tsx:56
|
#: packages/email/templates/reset-password.tsx:56
|
||||||
msgid "Hi, {userName} <0>({userEmail})</0>"
|
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
|
#: packages/lib/constants/recipient-roles.ts:47
|
||||||
msgid "I am a viewer of this document"
|
msgid "I am a viewer of this document"
|
||||||
msgstr "Je suis un lecteur de ce document"
|
msgstr "Je suis un visualiseur de ce document"
|
||||||
|
|
||||||
#: packages/lib/constants/recipient-roles.ts:45
|
#: packages/lib/constants/recipient-roles.ts:45
|
||||||
msgid "I am an approver of this document"
|
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
|
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx:209
|
||||||
msgid "Max"
|
msgid "Max"
|
||||||
msgstr "Maximum"
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:227
|
#: 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."
|
msgid "Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use template defaults."
|
||||||
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."
|
msgstr "Taille maximale du fichier : 4 Mo. Maximum de 100 lignes par téléversement. Les valeurs vides utiliseront les valeurs par défaut du modèle."
|
||||||
|
|
||||||
#: packages/lib/constants/teams.ts:12
|
#: packages/lib/constants/teams.ts:12
|
||||||
msgid "Member"
|
msgid "Member"
|
||||||
@ -5032,7 +5032,7 @@ msgstr "Modèle supprimé"
|
|||||||
|
|
||||||
#: apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx:66
|
#: apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx:66
|
||||||
msgid "Template document uploaded"
|
msgid "Template document uploaded"
|
||||||
msgstr "Document modèle importé"
|
msgstr "Document modèle téléchargé"
|
||||||
|
|
||||||
#: apps/web/src/app/(dashboard)/templates/duplicate-template-dialog.tsx:41
|
#: apps/web/src/app/(dashboard)/templates/duplicate-template-dialog.tsx:41
|
||||||
msgid "Template duplicated"
|
msgid "Template duplicated"
|
||||||
@ -5097,11 +5097,11 @@ msgstr "Couleur du texte"
|
|||||||
|
|
||||||
#: apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx:24
|
#: 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."
|
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 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."
|
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."
|
||||||
|
|
||||||
#: packages/email/template-components/template-forgot-password.tsx:25
|
#: packages/email/template-components/template-forgot-password.tsx:25
|
||||||
msgid "That's okay, it happens! Click the button below to reset your password."
|
msgid "That's okay, it happens! Click the button below to reset your password."
|
||||||
msgstr "Ce n'est pas grave, cela arrive ! Cliquez sur le bouton ci-dessous pour réinitialiser votre mot de passe."
|
msgstr "C'est d'accord, 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
|
#: apps/web/src/app/(dashboard)/admin/users/[id]/delete-user-dialog.tsx:52
|
||||||
msgid "The account has been deleted successfully."
|
msgid "The account has been deleted successfully."
|
||||||
@ -5331,7 +5331,7 @@ msgstr "Le webhook a été créé avec succès."
|
|||||||
|
|
||||||
#: apps/web/src/app/(dashboard)/documents/empty-state.tsx:25
|
#: 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."
|
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 importer un document pour commencer un brouillon."
|
msgstr "Il n'y a pas de brouillons actifs pour le moment. Vous pouvez télécharger un document pour commencer à rédiger."
|
||||||
|
|
||||||
#: apps/web/src/app/(dashboard)/documents/empty-state.tsx:20
|
#: 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."
|
msgid "There are no completed documents yet. Documents that you have created or received will appear here once completed."
|
||||||
@ -5899,27 +5899,27 @@ msgstr "Améliorer"
|
|||||||
|
|
||||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:132
|
#: 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."
|
msgid "Upload a CSV file to create multiple documents from this template. Each row represents one document with its recipient details."
|
||||||
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."
|
msgstr "Téléchargez un fichier CSV pour créer plusieurs documents à partir de ce modèle. Chaque ligne représente un document avec ses détails de destinataire."
|
||||||
|
|
||||||
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:426
|
#: 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"
|
msgid "Upload a custom document to use instead of the template's default document"
|
||||||
msgstr "Importer un document personnalisé à utiliser à la place du modèle par défaut"
|
msgstr "Téléchargez un document personnalisé à utiliser à la place du document par défaut du modèle"
|
||||||
|
|
||||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:267
|
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:267
|
||||||
msgid "Upload and Process"
|
msgid "Upload and Process"
|
||||||
msgstr "Importer et traiter"
|
msgstr "Télécharger et traiter"
|
||||||
|
|
||||||
#: apps/web/src/components/forms/avatar-image.tsx:179
|
#: apps/web/src/components/forms/avatar-image.tsx:179
|
||||||
msgid "Upload Avatar"
|
msgid "Upload Avatar"
|
||||||
msgstr "Importer un avatar"
|
msgstr "Télécharger un avatar"
|
||||||
|
|
||||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:198
|
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:198
|
||||||
msgid "Upload CSV"
|
msgid "Upload CSV"
|
||||||
msgstr "Importer le CSV"
|
msgstr "Télécharger le CSV"
|
||||||
|
|
||||||
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:419
|
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:419
|
||||||
msgid "Upload custom document"
|
msgid "Upload custom document"
|
||||||
msgstr "Importer un document personnalisé"
|
msgstr "Télécharger un document personnalisé"
|
||||||
|
|
||||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx:529
|
#: packages/ui/primitives/signature-pad/signature-pad.tsx:529
|
||||||
msgid "Upload Signature"
|
msgid "Upload Signature"
|
||||||
@ -5927,7 +5927,7 @@ msgstr "Importer une signature"
|
|||||||
|
|
||||||
#: packages/ui/primitives/document-dropzone.tsx:70
|
#: packages/ui/primitives/document-dropzone.tsx:70
|
||||||
msgid "Upload Template Document"
|
msgid "Upload Template Document"
|
||||||
msgstr "Importer le document modèle"
|
msgstr "Télécharger le document modèle"
|
||||||
|
|
||||||
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/branding-preferences.tsx:256
|
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/branding-preferences.tsx:256
|
||||||
msgid "Upload your brand logo (max 5MB, JPG, PNG, or WebP)"
|
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
|
#: apps/web/src/components/forms/avatar-image.tsx:91
|
||||||
msgid "Uploaded file is too large"
|
msgid "Uploaded file is too large"
|
||||||
msgstr "Le fichier importé est trop volumineux"
|
msgstr "Le fichier téléchargé est trop volumineux"
|
||||||
|
|
||||||
#: apps/web/src/components/forms/avatar-image.tsx:92
|
#: apps/web/src/components/forms/avatar-image.tsx:92
|
||||||
msgid "Uploaded file is too small"
|
msgid "Uploaded file is too small"
|
||||||
msgstr "Le fichier importé est trop petit"
|
msgstr "Le fichier téléchargé est trop petit"
|
||||||
|
|
||||||
#: apps/web/src/components/forms/avatar-image.tsx:93
|
#: apps/web/src/components/forms/avatar-image.tsx:93
|
||||||
msgid "Uploaded file not an allowed file type"
|
msgid "Uploaded file not an allowed file type"
|
||||||
msgstr "Le fichier importé n'est pas un type de fichier autorisé"
|
msgstr "Le fichier téléchargé n'est pas un type de fichier autorisé"
|
||||||
|
|
||||||
#: apps/web/src/app/(dashboard)/templates/[id]/template-page-view.tsx:175
|
#: apps/web/src/app/(dashboard)/templates/[id]/template-page-view.tsx:175
|
||||||
msgid "Use"
|
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
|
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:60
|
||||||
msgid "Verify your email to upload documents."
|
msgid "Verify your email to upload documents."
|
||||||
msgstr "Vérifiez votre e-mail pour importer des documents."
|
msgstr "Vérifiez votre e-mail pour télécharger des documents."
|
||||||
|
|
||||||
#: packages/email/templates/confirm-team-email.tsx:71
|
#: packages/email/templates/confirm-team-email.tsx:71
|
||||||
msgid "Verify your team email address"
|
msgid "Verify your team email address"
|
||||||
@ -6137,11 +6137,11 @@ msgstr "Vu"
|
|||||||
|
|
||||||
#: packages/lib/constants/recipient-roles.ts:32
|
#: packages/lib/constants/recipient-roles.ts:32
|
||||||
msgid "Viewer"
|
msgid "Viewer"
|
||||||
msgstr "Lecteur"
|
msgstr "Visiteur"
|
||||||
|
|
||||||
#: packages/lib/constants/recipient-roles.ts:33
|
#: packages/lib/constants/recipient-roles.ts:33
|
||||||
msgid "Viewers"
|
msgid "Viewers"
|
||||||
msgstr "Lecteurs"
|
msgstr "Spectateurs"
|
||||||
|
|
||||||
#: packages/lib/constants/recipient-roles.ts:31
|
#: packages/lib/constants/recipient-roles.ts:31
|
||||||
msgid "Viewing"
|
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
|
#: apps/web/src/app/(dashboard)/settings/billing/page.tsx:80
|
||||||
msgid "You are currently on the <0>Free Plan</0>."
|
msgid "You are currently on the <0>Free Plan</0>."
|
||||||
msgstr "Vous êtes actuellement sur l'<0>Abonnement Gratuit</0>."
|
msgstr "Vous êtes actuellement sur le <0>Plan Gratuit</0>."
|
||||||
|
|
||||||
#: apps/web/src/components/(teams)/dialogs/update-team-member-dialog.tsx:148
|
#: apps/web/src/components/(teams)/dialogs/update-team-member-dialog.tsx:148
|
||||||
msgid "You are currently updating <0>{teamMemberName}.</0>"
|
msgid "You are currently updating <0>{teamMemberName}.</0>"
|
||||||
@ -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
|
#: packages/ui/primitives/document-dropzone.tsx:43
|
||||||
msgid "You cannot upload documents at this time."
|
msgid "You cannot upload documents at this time."
|
||||||
msgstr "Vous ne pouvez pas importer de documents pour le moment."
|
msgstr "Vous ne pouvez pas télécharger de documents pour le moment."
|
||||||
|
|
||||||
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:103
|
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:103
|
||||||
msgid "You cannot upload encrypted PDFs"
|
msgid "You cannot upload encrypted PDFs"
|
||||||
msgstr "Vous ne pouvez pas importer de PDF cryptés"
|
msgstr "Vous ne pouvez pas télécharger de PDF cryptés"
|
||||||
|
|
||||||
#: apps/web/src/app/(dashboard)/settings/billing/billing-portal-button.tsx:46
|
#: 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."
|
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
|
#: 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."
|
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 importer un."
|
msgstr "Vous n'avez pas encore créé de modèles. Pour créer un modèle, veuillez en télécharger un."
|
||||||
|
|
||||||
#: apps/web/src/app/(dashboard)/documents/empty-state.tsx:30
|
#: 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."
|
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 importer un."
|
msgstr "Vous n'avez pas encore créé ou reçu de documents. Pour créer un document, veuillez en télécharger un."
|
||||||
|
|
||||||
#: apps/web/src/app/(dashboard)/templates/template-direct-link-dialog.tsx:229
|
#: 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>"
|
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
|
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:106
|
||||||
msgid "You have reached your document limit for this month. Please upgrade your plan."
|
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 à l'abonnement supérieur."
|
msgstr "Vous avez atteint votre limite de documents pour ce mois. Veuillez passer à un plan supérieur."
|
||||||
|
|
||||||
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:56
|
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:56
|
||||||
#: packages/ui/primitives/document-dropzone.tsx:69
|
#: packages/ui/primitives/document-dropzone.tsx:69
|
||||||
@ -6811,11 +6811,11 @@ msgstr "Votre envoi groupé a été initié. Vous recevrez une notification par
|
|||||||
|
|
||||||
#: packages/email/templates/bulk-send-complete.tsx:40
|
#: packages/email/templates/bulk-send-complete.tsx:40
|
||||||
msgid "Your bulk send operation for template \"{templateName}\" has completed."
|
msgid "Your bulk send operation for template \"{templateName}\" has completed."
|
||||||
msgstr "Votre envoi groupé pour le modèle \"{templateName}\" est terminé."
|
msgstr "Votre opération d'envoi groupé pour le modèle \"{templateName}\" est terminée."
|
||||||
|
|
||||||
#: apps/web/src/app/(dashboard)/settings/billing/page.tsx:125
|
#: apps/web/src/app/(dashboard)/settings/billing/page.tsx:125
|
||||||
msgid "Your current plan is past due. Please update your payment information."
|
msgid "Your current plan is past due. Please update your payment information."
|
||||||
msgstr "Votre abonnement actuel est arrivé à échéance. Veuillez mettre à jour vos informations de paiement."
|
msgstr "Votre plan actuel est en retard. Veuillez mettre à jour vos informations de paiement."
|
||||||
|
|
||||||
#: apps/web/src/components/templates/manage-public-template-dialog.tsx:249
|
#: apps/web/src/components/templates/manage-public-template-dialog.tsx:249
|
||||||
msgid "Your direct signing templates"
|
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
|
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:123
|
||||||
msgid "Your document failed to upload."
|
msgid "Your document failed to upload."
|
||||||
msgstr "L'importation de votre document a échoué."
|
msgstr "Votre document a échoué à se télécharger."
|
||||||
|
|
||||||
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:169
|
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:169
|
||||||
msgid "Your document has been created from the template successfully."
|
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
|
#: apps/web/src/app/(dashboard)/documents/upload-document.tsx:86
|
||||||
msgid "Your document has been uploaded successfully."
|
msgid "Your document has been uploaded successfully."
|
||||||
msgstr "Votre document a été importé avec succès."
|
msgstr "Votre document a été téléchargé avec succès."
|
||||||
|
|
||||||
#: apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx:68
|
#: 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."
|
msgid "Your document has been uploaded successfully. You will be redirected to the template page."
|
||||||
msgstr "Votre document a été importé avec succès. Vous serez redirigé vers la page de modèle."
|
msgstr "Votre document a été téléchargé 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
|
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:104
|
||||||
msgid "Your document preferences have been updated"
|
msgid "Your document preferences have been updated"
|
||||||
|
|||||||
@ -29,9 +29,7 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
|
|||||||
'DOCUMENT_FIELD_INSERTED', // When a field is inserted (signed/approved/etc) by a recipient.
|
'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_UNINSERTED', // When a field is uninserted by a recipient.
|
||||||
'DOCUMENT_FIELD_PREFILLED', // When a field is prefilled by an assistant.
|
'DOCUMENT_FIELD_PREFILLED', // When a field is prefilled by an assistant.
|
||||||
'DOCUMENT_VISIBILITY_UPDATED', // When the document visibility scope is updated.
|
'DOCUMENT_VISIBILITY_UPDATED', // When the document visibility scope is updated
|
||||||
'DOCUMENT_SIGNING_CERTIFICATE_UPDATED', // When the include signing certificate is updated.
|
|
||||||
'DOCUMENT_AUDIT_TRAIL_UPDATED', // When the include audit trail is updated.
|
|
||||||
'DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED', // When the global access authentication 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.
|
'DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED', // When the global action authentication is updated.
|
||||||
'DOCUMENT_META_UPDATED', // When the document meta data is updated.
|
'DOCUMENT_META_UPDATED', // When the document meta data is updated.
|
||||||
@ -399,16 +397,6 @@ export const ZDocumentAuditLogEventDocumentVisibilitySchema = z.object({
|
|||||||
data: ZGenericFromToSchema,
|
data: ZGenericFromToSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZDocumentAuditLogEventDocumentSigningCertificateUpdatedSchema = z.object({
|
|
||||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_CERTIFICATE_UPDATED),
|
|
||||||
data: ZGenericFromToSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ZDocumentAuditLogEventDocumentAuditTrailUpdatedSchema = z.object({
|
|
||||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_AUDIT_TRAIL_UPDATED),
|
|
||||||
data: ZGenericFromToSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event: Document global authentication access updated.
|
* Event: Document global authentication access updated.
|
||||||
*/
|
*/
|
||||||
@ -586,8 +574,6 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
|
|||||||
ZDocumentAuditLogEventDocumentFieldUninsertedSchema,
|
ZDocumentAuditLogEventDocumentFieldUninsertedSchema,
|
||||||
ZDocumentAuditLogEventDocumentFieldPrefilledSchema,
|
ZDocumentAuditLogEventDocumentFieldPrefilledSchema,
|
||||||
ZDocumentAuditLogEventDocumentVisibilitySchema,
|
ZDocumentAuditLogEventDocumentVisibilitySchema,
|
||||||
ZDocumentAuditLogEventDocumentSigningCertificateUpdatedSchema,
|
|
||||||
ZDocumentAuditLogEventDocumentAuditTrailUpdatedSchema,
|
|
||||||
ZDocumentAuditLogEventDocumentGlobalAuthAccessUpdatedSchema,
|
ZDocumentAuditLogEventDocumentGlobalAuthAccessUpdatedSchema,
|
||||||
ZDocumentAuditLogEventDocumentGlobalAuthActionUpdatedSchema,
|
ZDocumentAuditLogEventDocumentGlobalAuthActionUpdatedSchema,
|
||||||
ZDocumentAuditLogEventDocumentMetaUpdatedSchema,
|
ZDocumentAuditLogEventDocumentMetaUpdatedSchema,
|
||||||
|
|||||||
@ -18,8 +18,6 @@ import { ZRecipientLiteSchema } from './recipient';
|
|||||||
*/
|
*/
|
||||||
export const ZDocumentSchema = DocumentSchema.pick({
|
export const ZDocumentSchema = DocumentSchema.pick({
|
||||||
visibility: true,
|
visibility: true,
|
||||||
includeSigningCertificate: true,
|
|
||||||
includeAuditTrailLog: true,
|
|
||||||
status: true,
|
status: true,
|
||||||
source: true,
|
source: true,
|
||||||
id: true,
|
id: true,
|
||||||
@ -57,6 +55,7 @@ export const ZDocumentSchema = DocumentSchema.pick({
|
|||||||
typedSignatureEnabled: true,
|
typedSignatureEnabled: true,
|
||||||
language: true,
|
language: true,
|
||||||
emailSettings: true,
|
emailSettings: true,
|
||||||
|
modifyNextSigner: true,
|
||||||
}).nullable(),
|
}).nullable(),
|
||||||
recipients: ZRecipientLiteSchema.array(),
|
recipients: ZRecipientLiteSchema.array(),
|
||||||
fields: ZFieldSchema.array(),
|
fields: ZFieldSchema.array(),
|
||||||
@ -84,8 +83,6 @@ export const ZDocumentLiteSchema = DocumentSchema.pick({
|
|||||||
deletedAt: true,
|
deletedAt: true,
|
||||||
teamId: true,
|
teamId: true,
|
||||||
templateId: true,
|
templateId: true,
|
||||||
includeSigningCertificate: true,
|
|
||||||
includeAuditTrailLog: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -108,8 +105,6 @@ export const ZDocumentManySchema = DocumentSchema.pick({
|
|||||||
deletedAt: true,
|
deletedAt: true,
|
||||||
teamId: true,
|
teamId: true,
|
||||||
templateId: true,
|
templateId: true,
|
||||||
includeSigningCertificate: true,
|
|
||||||
includeAuditTrailLog: true,
|
|
||||||
}).extend({
|
}).extend({
|
||||||
user: UserSchema.pick({
|
user: UserSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
|
|||||||
@ -123,42 +123,6 @@ export const ZFieldMetaNotOptionalSchema = z.discriminatedUnion('type', [
|
|||||||
|
|
||||||
export type TFieldMetaNotOptionalSchema = z.infer<typeof ZFieldMetaNotOptionalSchema>;
|
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
|
export const ZFieldMetaSchema = z
|
||||||
.union([
|
.union([
|
||||||
// Handles an empty object being provided as fieldMeta.
|
// Handles an empty object being provided as fieldMeta.
|
||||||
|
|||||||
@ -322,14 +322,6 @@ export const formatDocumentAuditLogAction = (
|
|||||||
anonymous: msg`Document visibility updated`,
|
anonymous: msg`Document visibility updated`,
|
||||||
identified: msg`${prefix} updated the document visibility`,
|
identified: msg`${prefix} updated the document visibility`,
|
||||||
}))
|
}))
|
||||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_CERTIFICATE_UPDATED }, () => ({
|
|
||||||
anonymous: msg`Document signing certificate updated`,
|
|
||||||
identified: msg`${prefix} updated the document signing certificate`,
|
|
||||||
}))
|
|
||||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_AUDIT_TRAIL_UPDATED }, () => ({
|
|
||||||
anonymous: msg`Document audit trail updated`,
|
|
||||||
identified: msg`${prefix} updated the document audit trail`,
|
|
||||||
}))
|
|
||||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED }, () => ({
|
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED }, () => ({
|
||||||
anonymous: msg`Document access auth updated`,
|
anonymous: msg`Document access auth updated`,
|
||||||
identified: msg`${prefix} updated the document access auth requirements`,
|
identified: msg`${prefix} updated the document access auth requirements`,
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "DocumentMeta" ADD COLUMN "modifyNextSigner" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "TemplateMeta" ADD COLUMN "modifyNextSigner" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@ -1,2 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "TeamGlobalSettings" ADD COLUMN "includeAuditTrailLog" BOOLEAN NOT NULL DEFAULT false;
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
/*
|
|
||||||
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;
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "Document" ADD COLUMN "includeAuditTrail" BOOLEAN NOT NULL DEFAULT true,
|
|
||||||
ADD COLUMN "includeSigningCertificate" BOOLEAN NOT NULL DEFAULT true;
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- You are about to drop the column `includeAuditTrail` on the `Document` table. All the data in the column will be lost.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "Document" DROP COLUMN "includeAuditTrail",
|
|
||||||
ADD COLUMN "includeAuditTrailLog" BOOLEAN NOT NULL DEFAULT false;
|
|
||||||
@ -270,25 +270,18 @@ model Account {
|
|||||||
scope String?
|
scope String?
|
||||||
id_token String? @db.Text
|
id_token String? @db.Text
|
||||||
session_state String?
|
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])
|
@@unique([provider, providerAccountId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Session {
|
model Session {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
sessionToken String @unique
|
sessionToken String @unique
|
||||||
userId Int
|
userId Int
|
||||||
|
expires DateTime
|
||||||
ipAddress String?
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
userAgent String?
|
|
||||||
expiresAt DateTime
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DocumentStatus {
|
enum DocumentStatus {
|
||||||
@ -311,32 +304,30 @@ enum DocumentVisibility {
|
|||||||
|
|
||||||
/// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';", "import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';"])
|
/// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';", "import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';"])
|
||||||
model Document {
|
model Document {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
externalId String? /// @zod.string.describe("A custom external ID you can use to identify the document.")
|
externalId String? /// @zod.string.describe("A custom external ID you can use to identify the document.")
|
||||||
userId Int /// @zod.number.describe("The ID of the user that created this document.")
|
userId Int /// @zod.number.describe("The ID of the user that created this document.")
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema)
|
authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema)
|
||||||
formValues Json? /// [DocumentFormValues] @zod.custom.use(ZDocumentFormValuesSchema)
|
formValues Json? /// [DocumentFormValues] @zod.custom.use(ZDocumentFormValuesSchema)
|
||||||
visibility DocumentVisibility @default(EVERYONE)
|
visibility DocumentVisibility @default(EVERYONE)
|
||||||
includeSigningCertificate Boolean @default(true)
|
title String
|
||||||
includeAuditTrailLog Boolean @default(false)
|
status DocumentStatus @default(DRAFT)
|
||||||
title String
|
recipients Recipient[]
|
||||||
status DocumentStatus @default(DRAFT)
|
fields Field[]
|
||||||
recipients Recipient[]
|
shareLinks DocumentShareLink[]
|
||||||
fields Field[]
|
documentDataId String
|
||||||
shareLinks DocumentShareLink[]
|
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
|
||||||
documentDataId String
|
documentMeta DocumentMeta?
|
||||||
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
|
createdAt DateTime @default(now())
|
||||||
documentMeta DocumentMeta?
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
createdAt DateTime @default(now())
|
completedAt DateTime?
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
deletedAt DateTime?
|
||||||
completedAt DateTime?
|
teamId Int?
|
||||||
deletedAt DateTime?
|
team Team? @relation(fields: [teamId], references: [id])
|
||||||
teamId Int?
|
templateId Int?
|
||||||
team Team? @relation(fields: [teamId], references: [id])
|
template Template? @relation(fields: [templateId], references: [id], onDelete: SetNull)
|
||||||
templateId Int?
|
source DocumentSource
|
||||||
template Template? @relation(fields: [templateId], references: [id], onDelete: SetNull)
|
|
||||||
source DocumentSource
|
|
||||||
|
|
||||||
auditLogs DocumentAuditLog[]
|
auditLogs DocumentAuditLog[]
|
||||||
|
|
||||||
@ -399,6 +390,7 @@ model DocumentMeta {
|
|||||||
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||||
redirectUrl String?
|
redirectUrl String?
|
||||||
signingOrder DocumentSigningOrder @default(PARALLEL)
|
signingOrder DocumentSigningOrder @default(PARALLEL)
|
||||||
|
modifyNextSigner Boolean @default(false)
|
||||||
typedSignatureEnabled Boolean @default(true)
|
typedSignatureEnabled Boolean @default(true)
|
||||||
language String @default("en")
|
language String @default("en")
|
||||||
distributionMethod DocumentDistributionMethod @default(EMAIL)
|
distributionMethod DocumentDistributionMethod @default(EMAIL)
|
||||||
@ -545,7 +537,6 @@ model TeamGlobalSettings {
|
|||||||
includeSenderDetails Boolean @default(true)
|
includeSenderDetails Boolean @default(true)
|
||||||
typedSignatureEnabled Boolean @default(true)
|
typedSignatureEnabled Boolean @default(true)
|
||||||
includeSigningCertificate Boolean @default(true)
|
includeSigningCertificate Boolean @default(true)
|
||||||
includeAuditTrailLog Boolean @default(false)
|
|
||||||
|
|
||||||
brandingEnabled Boolean @default(false)
|
brandingEnabled Boolean @default(false)
|
||||||
brandingLogo String @default("")
|
brandingLogo String @default("")
|
||||||
@ -669,6 +660,7 @@ model TemplateMeta {
|
|||||||
signingOrder DocumentSigningOrder? @default(PARALLEL)
|
signingOrder DocumentSigningOrder? @default(PARALLEL)
|
||||||
typedSignatureEnabled Boolean @default(true)
|
typedSignatureEnabled Boolean @default(true)
|
||||||
distributionMethod DocumentDistributionMethod @default(EMAIL)
|
distributionMethod DocumentDistributionMethod @default(EMAIL)
|
||||||
|
modifyNextSigner Boolean @default(false)
|
||||||
|
|
||||||
templateId Int @unique
|
templateId Int @unique
|
||||||
template Template @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
template Template @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
||||||
|
|||||||
@ -65,7 +65,7 @@ export const documentRouter = router({
|
|||||||
.input(ZGetDocumentByIdQuerySchema)
|
.input(ZGetDocumentByIdQuerySchema)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const { teamId } = ctx;
|
const { teamId } = ctx;
|
||||||
const { documentId, includeCertificate, includeAuditLog } = input;
|
const { documentId } = input;
|
||||||
|
|
||||||
return await getDocumentById({
|
return await getDocumentById({
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
@ -266,15 +266,14 @@ export const documentRouter = router({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*
|
|
||||||
* Todo: Refactor to updateDocument.
|
|
||||||
*/
|
*/
|
||||||
setSettingsForDocument: authenticatedProcedure
|
updateDocument: authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/document/update',
|
path: '/document/update',
|
||||||
summary: 'Update document',
|
summary: 'Update document',
|
||||||
|
description: 'Update an existing document',
|
||||||
tags: ['Document'],
|
tags: ['Document'],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -286,9 +285,9 @@ export const documentRouter = router({
|
|||||||
|
|
||||||
const userId = ctx.user.id;
|
const userId = ctx.user.id;
|
||||||
|
|
||||||
if (Object.values(meta).length > 0) {
|
if (Object.keys(meta).length > 0) {
|
||||||
await upsertDocumentMeta({
|
await upsertDocumentMeta({
|
||||||
userId: ctx.user.id,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
documentId,
|
documentId,
|
||||||
subject: meta.subject,
|
subject: meta.subject,
|
||||||
@ -301,6 +300,7 @@ export const documentRouter = router({
|
|||||||
distributionMethod: meta.distributionMethod,
|
distributionMethod: meta.distributionMethod,
|
||||||
signingOrder: meta.signingOrder,
|
signingOrder: meta.signingOrder,
|
||||||
emailSettings: meta.emailSettings,
|
emailSettings: meta.emailSettings,
|
||||||
|
modifyNextSigner: meta.modifyNextSigner,
|
||||||
requestMetadata: ctx.metadata,
|
requestMetadata: ctx.metadata,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,16 +63,6 @@ export const ZDocumentVisibilitySchema = z
|
|||||||
.nativeEnum(DocumentVisibility)
|
.nativeEnum(DocumentVisibility)
|
||||||
.describe('The visibility of the document.');
|
.describe('The visibility of the document.');
|
||||||
|
|
||||||
export const ZDocumentIncludeSigningCertificateSchema = z
|
|
||||||
.boolean()
|
|
||||||
.default(true)
|
|
||||||
.describe('Whether to include a signing certificate in the document.');
|
|
||||||
|
|
||||||
export const ZDocumentIncludeAuditTrailSchema = z
|
|
||||||
.boolean()
|
|
||||||
.default(true)
|
|
||||||
.describe('Whether to include an audit trail in the document.');
|
|
||||||
|
|
||||||
export const ZDocumentMetaTimezoneSchema = z
|
export const ZDocumentMetaTimezoneSchema = z
|
||||||
.string()
|
.string()
|
||||||
.describe(
|
.describe(
|
||||||
@ -151,8 +141,6 @@ export const ZFindDocumentAuditLogsQuerySchema = ZFindSearchParamsSchema.extend(
|
|||||||
|
|
||||||
export const ZGetDocumentByIdQuerySchema = z.object({
|
export const ZGetDocumentByIdQuerySchema = z.object({
|
||||||
documentId: z.number(),
|
documentId: z.number(),
|
||||||
includeCertificate: z.boolean().default(true).optional(),
|
|
||||||
includeAuditLog: z.boolean().default(true).optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZDuplicateDocumentRequestSchema = z.object({
|
export const ZDuplicateDocumentRequestSchema = z.object({
|
||||||
@ -247,8 +235,6 @@ export const ZUpdateDocumentRequestSchema = z.object({
|
|||||||
title: ZDocumentTitleSchema.optional(),
|
title: ZDocumentTitleSchema.optional(),
|
||||||
externalId: ZDocumentExternalIdSchema.nullish(),
|
externalId: ZDocumentExternalIdSchema.nullish(),
|
||||||
visibility: ZDocumentVisibilitySchema.optional(),
|
visibility: ZDocumentVisibilitySchema.optional(),
|
||||||
includeSigningCertificate: ZDocumentIncludeSigningCertificateSchema.optional(),
|
|
||||||
includeAuditTrailLog: ZDocumentIncludeAuditTrailSchema.optional(),
|
|
||||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullish(),
|
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullish(),
|
||||||
globalActionAuth: ZDocumentActionAuthTypesSchema.nullish(),
|
globalActionAuth: ZDocumentActionAuthTypesSchema.nullish(),
|
||||||
})
|
})
|
||||||
@ -265,6 +251,7 @@ export const ZUpdateDocumentRequestSchema = z.object({
|
|||||||
language: ZDocumentMetaLanguageSchema.optional(),
|
language: ZDocumentMetaLanguageSchema.optional(),
|
||||||
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||||
|
modifyNextSigner: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -452,7 +452,7 @@ export const fieldRouter = router({
|
|||||||
return await signFieldWithToken({
|
return await signFieldWithToken({
|
||||||
token,
|
token,
|
||||||
fieldId,
|
fieldId,
|
||||||
value: value ?? '',
|
value,
|
||||||
isBase64,
|
isBase64,
|
||||||
userId: ctx.user?.id,
|
userId: ctx.user?.id,
|
||||||
authOptions,
|
authOptions,
|
||||||
|
|||||||
@ -153,7 +153,7 @@ export const ZSetFieldsForTemplateResponseSchema = z.object({
|
|||||||
export const ZSignFieldWithTokenMutationSchema = z.object({
|
export const ZSignFieldWithTokenMutationSchema = z.object({
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
fieldId: z.number(),
|
fieldId: z.number(),
|
||||||
value: z.string().trim().optional(),
|
value: z.string().trim(),
|
||||||
isBase64: z.boolean().optional(),
|
isBase64: z.boolean().optional(),
|
||||||
authOptions: ZRecipientActionAuthSchema.optional(),
|
authOptions: ZRecipientActionAuthSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -437,13 +437,14 @@ export const recipientRouter = router({
|
|||||||
completeDocumentWithToken: procedure
|
completeDocumentWithToken: procedure
|
||||||
.input(ZCompleteDocumentWithTokenMutationSchema)
|
.input(ZCompleteDocumentWithTokenMutationSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { token, documentId, authOptions } = input;
|
const { token, documentId, authOptions, nextSigner } = input;
|
||||||
|
|
||||||
return await completeDocumentWithToken({
|
return await completeDocumentWithToken({
|
||||||
token,
|
token,
|
||||||
documentId,
|
documentId,
|
||||||
authOptions,
|
authOptions,
|
||||||
userId: ctx.user?.id,
|
userId: ctx.user?.id,
|
||||||
|
nextSigner,
|
||||||
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -212,6 +212,12 @@ export const ZCompleteDocumentWithTokenMutationSchema = z.object({
|
|||||||
token: z.string(),
|
token: z.string(),
|
||||||
documentId: z.number(),
|
documentId: z.number(),
|
||||||
authOptions: ZRecipientActionAuthSchema.optional(),
|
authOptions: ZRecipientActionAuthSchema.optional(),
|
||||||
|
nextSigner: z
|
||||||
|
.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
name: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TCompleteDocumentWithTokenMutationSchema = z.infer<
|
export type TCompleteDocumentWithTokenMutationSchema = z.infer<
|
||||||
|
|||||||
@ -206,7 +206,6 @@ export const ZUpdateTeamDocumentSettingsMutationSchema = z.object({
|
|||||||
includeSenderDetails: z.boolean().optional().default(false),
|
includeSenderDetails: z.boolean().optional().default(false),
|
||||||
typedSignatureEnabled: z.boolean().optional().default(true),
|
typedSignatureEnabled: z.boolean().optional().default(true),
|
||||||
includeSigningCertificate: z.boolean().optional().default(true),
|
includeSigningCertificate: z.boolean().optional().default(true),
|
||||||
includeAuditTrailLog: z.boolean().optional().default(true),
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -227,8 +227,7 @@ export const templateRouter = router({
|
|||||||
.output(ZCreateDocumentFromTemplateResponseSchema)
|
.output(ZCreateDocumentFromTemplateResponseSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const { teamId } = ctx;
|
const { teamId } = ctx;
|
||||||
const { templateId, recipients, distributeDocument, customDocumentDataId, prefillFields } =
|
const { templateId, recipients, distributeDocument, customDocumentDataId } = input;
|
||||||
input;
|
|
||||||
|
|
||||||
const limits = await getServerLimits({ email: ctx.user.email, teamId });
|
const limits = await getServerLimits({ email: ctx.user.email, teamId });
|
||||||
|
|
||||||
@ -243,7 +242,6 @@ export const templateRouter = router({
|
|||||||
recipients,
|
recipients,
|
||||||
customDocumentDataId,
|
customDocumentDataId,
|
||||||
requestMetadata: ctx.metadata,
|
requestMetadata: ctx.metadata,
|
||||||
prefillFields,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (distributeDocument) {
|
if (distributeDocument) {
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import {
|
|||||||
ZDocumentActionAuthTypesSchema,
|
ZDocumentActionAuthTypesSchema,
|
||||||
} from '@documenso/lib/types/document-auth';
|
} from '@documenso/lib/types/document-auth';
|
||||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||||
import { ZFieldMetaPrefillFieldsSchema } from '@documenso/lib/types/field-meta';
|
|
||||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||||
import {
|
import {
|
||||||
ZTemplateLiteSchema,
|
ZTemplateLiteSchema,
|
||||||
@ -68,7 +67,6 @@ export const ZCreateDocumentFromTemplateRequestSchema = z.object({
|
|||||||
'The data ID of an alternative PDF to use when creating the document. If not provided, the PDF attached to the template will be used.',
|
'The data ID of an alternative PDF to use when creating the document. If not provided, the PDF attached to the template will be used.',
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
prefillFields: z.array(ZFieldMetaPrefillFieldsSchema).optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZCreateDocumentFromTemplateResponseSchema = ZDocumentSchema;
|
export const ZCreateDocumentFromTemplateResponseSchema = ZDocumentSchema;
|
||||||
|
|||||||
@ -42,7 +42,6 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@documenso/ui/primitives/form/form';
|
} from '@documenso/ui/primitives/form/form';
|
||||||
|
|
||||||
import { Checkbox } from '../checkbox';
|
|
||||||
import { Combobox } from '../combobox';
|
import { Combobox } from '../combobox';
|
||||||
import { Input } from '../input';
|
import { Input } from '../input';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select';
|
||||||
@ -93,8 +92,6 @@ export const AddSettingsFormPartial = ({
|
|||||||
visibility: document.visibility || '',
|
visibility: document.visibility || '',
|
||||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
|
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
|
||||||
globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
|
globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
|
||||||
includeSigningCertificate: document.includeSigningCertificate ?? true,
|
|
||||||
includeAuditTrailLog: document.includeAuditTrailLog ?? true,
|
|
||||||
meta: {
|
meta: {
|
||||||
timezone:
|
timezone:
|
||||||
TIME_ZONES.find((timezone) => timezone === document.documentMeta?.timezone) ??
|
TIME_ZONES.find((timezone) => timezone === document.documentMeta?.timezone) ??
|
||||||
@ -262,111 +259,6 @@ export const AddSettingsFormPartial = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="globalActionAuth"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="flex flex-row items-center">
|
|
||||||
<Trans>Recipient action authentication</Trans>
|
|
||||||
<DocumentGlobalAuthActionTooltip />
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormControl>
|
|
||||||
<DocumentGlobalAuthActionSelect {...field} onValueChange={field.onChange} />
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Accordion type="multiple" className="mt-6">
|
|
||||||
<AccordionItem value="advanced-options" className="border-none">
|
|
||||||
<AccordionTrigger className="text-foreground mb-2 rounded border px-3 py-2 text-left hover:bg-neutral-200/30 hover:no-underline">
|
|
||||||
<Trans>Certificates</Trans>
|
|
||||||
</AccordionTrigger>
|
|
||||||
|
|
||||||
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-2 text-sm leading-relaxed">
|
|
||||||
<div className="flex flex-col space-y-6">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="includeSigningCertificate"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex flex-row items-center gap-4">
|
|
||||||
<FormControl>
|
|
||||||
<Checkbox
|
|
||||||
checked={field.value}
|
|
||||||
className="h-5 w-5"
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="m-0 flex flex-row items-center">
|
|
||||||
<Trans>Include signing certificate</Trans>{' '}
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="mx-2 h-4 w-4" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
|
|
||||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
|
||||||
<Trans>
|
|
||||||
Including the signing certificate means that the certificate
|
|
||||||
will be attached to the document. You won't be able to remove
|
|
||||||
it. <br />
|
|
||||||
<br />
|
|
||||||
If you don't include it, you can download it individually.
|
|
||||||
</Trans>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</FormLabel>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="includeAuditTrailLog"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex flex-row items-center gap-4">
|
|
||||||
<FormControl>
|
|
||||||
<Checkbox
|
|
||||||
checked={field.value}
|
|
||||||
className="h-5 w-5"
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="m-0 flex flex-row items-center">
|
|
||||||
<Trans>Include audit trail</Trans>{' '}
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="mx-2 h-4 w-4" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
|
|
||||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
|
||||||
<Trans>
|
|
||||||
Including the audit trail means that the log of all actions will
|
|
||||||
be attached to the document. You won't be able to remove it.{' '}
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
If you don't include it, you can download it individually.
|
|
||||||
</Trans>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</FormLabel>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
{isDocumentEnterprise && (
|
{isDocumentEnterprise && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@ -29,8 +29,6 @@ export const ZAddSettingsFormSchema = z.object({
|
|||||||
title: z.string().trim().min(1, { message: "Title can't be empty" }),
|
title: z.string().trim().min(1, { message: "Title can't be empty" }),
|
||||||
externalId: z.string().optional(),
|
externalId: z.string().optional(),
|
||||||
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
||||||
includeSigningCertificate: z.boolean().default(true).optional(),
|
|
||||||
includeAuditTrailLog: z.boolean().default(true).optional(),
|
|
||||||
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||||
ZDocumentAccessAuthTypesSchema.optional(),
|
ZDocumentAccessAuthTypesSchema.optional(),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -49,6 +49,7 @@ export type AddSignersFormProps = {
|
|||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
signingOrder?: DocumentSigningOrder | null;
|
signingOrder?: DocumentSigningOrder | null;
|
||||||
|
modifyNextSigner?: boolean | null;
|
||||||
isDocumentEnterprise: boolean;
|
isDocumentEnterprise: boolean;
|
||||||
onSubmit: (_data: TAddSignersFormSchema) => void;
|
onSubmit: (_data: TAddSignersFormSchema) => void;
|
||||||
isDocumentPdfLoaded: boolean;
|
isDocumentPdfLoaded: boolean;
|
||||||
@ -59,6 +60,7 @@ export const AddSignersFormPartial = ({
|
|||||||
recipients,
|
recipients,
|
||||||
fields,
|
fields,
|
||||||
signingOrder,
|
signingOrder,
|
||||||
|
modifyNextSigner,
|
||||||
isDocumentEnterprise,
|
isDocumentEnterprise,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
isDocumentPdfLoaded,
|
isDocumentPdfLoaded,
|
||||||
@ -107,6 +109,7 @@ export const AddSignersFormPartial = ({
|
|||||||
)
|
)
|
||||||
: defaultRecipients,
|
: defaultRecipients,
|
||||||
signingOrder: signingOrder || DocumentSigningOrder.PARALLEL,
|
signingOrder: signingOrder || DocumentSigningOrder.PARALLEL,
|
||||||
|
modifyNextSigner: modifyNextSigner ?? false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -404,6 +407,35 @@ export const AddSignersFormPartial = ({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{isSigningOrderSequential && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="modifyNextSigner"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="mb-6 flex flex-row items-center space-x-2 space-y-0">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
id="modifyNextSigner"
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
field.onChange(checked);
|
||||||
|
}}
|
||||||
|
disabled={isSubmitting || hasDocumentBeenSent}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormLabel
|
||||||
|
htmlFor="modifyNextSigner"
|
||||||
|
className="text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
<Trans>Modify next signer</Trans>
|
||||||
|
</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<DragDropContext
|
<DragDropContext
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
sensors={[
|
sensors={[
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export const ZAddSignersFormSchema = z
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||||
|
modifyNextSigner: z.boolean(),
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(schema) => {
|
(schema) => {
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import { match } from 'ts-pattern';
|
|||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
|
import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
|
||||||
import { FieldType } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
import { useSignerColors } from '../../lib/signer-colors';
|
import { useSignerColors } from '../../lib/signer-colors';
|
||||||
import { cn } from '../../lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
@ -186,35 +185,11 @@ export const FieldItem = ({
|
|||||||
() => hasFieldMetaValues('CHECKBOX', field.fieldMeta, ZCheckboxFieldMeta),
|
() => hasFieldMetaValues('CHECKBOX', field.fieldMeta, ZCheckboxFieldMeta),
|
||||||
[field.fieldMeta],
|
[field.fieldMeta],
|
||||||
);
|
);
|
||||||
|
|
||||||
const radioHasValues = useMemo(
|
const radioHasValues = useMemo(
|
||||||
() => hasFieldMetaValues('RADIO', field.fieldMeta, ZRadioFieldMeta),
|
() => hasFieldMetaValues('RADIO', field.fieldMeta, ZRadioFieldMeta),
|
||||||
[field.fieldMeta],
|
[field.fieldMeta],
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasCheckedValues = (fieldMeta: TFieldMetaSchema, type: FieldType) => {
|
|
||||||
if (!fieldMeta || (type !== FieldType.RADIO && type !== FieldType.CHECKBOX)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === FieldType.RADIO) {
|
|
||||||
const parsed = ZRadioFieldMeta.parse(fieldMeta);
|
|
||||||
return parsed.values?.some((value) => value.checked) ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === FieldType.CHECKBOX) {
|
|
||||||
const parsed = ZCheckboxFieldMeta.parse(fieldMeta);
|
|
||||||
return parsed.values?.some((value) => value.checked) ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fieldHasCheckedValues = useMemo(
|
|
||||||
() => hasCheckedValues(field.fieldMeta, field.type),
|
|
||||||
[field.fieldMeta, field.type],
|
|
||||||
);
|
|
||||||
|
|
||||||
const fixedSize = checkBoxHasValues || radioHasValues;
|
const fixedSize = checkBoxHasValues || radioHasValues;
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
@ -254,21 +229,6 @@ export const FieldItem = ({
|
|||||||
onMove?.(d.node);
|
onMove?.(d.node);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(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-primary border': !fieldHasCheckedValues,
|
|
||||||
'bg-documenso-200 border-primary border': fieldHasCheckedValues,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{field.fieldMeta.label}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex h-full w-full items-center justify-center bg-white',
|
'relative flex h-full w-full items-center justify-center bg-white',
|
||||||
|
|||||||
@ -126,18 +126,6 @@ export const CheckboxFieldAdvancedSettings = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="mb-2">
|
|
||||||
<Label>
|
|
||||||
<Trans>Label</Trans>
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="label"
|
|
||||||
className="bg-background mt-2"
|
|
||||||
placeholder={_(msg`Field label`)}
|
|
||||||
value={fieldState.label}
|
|
||||||
onChange={(e) => handleFieldChange('label', e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row items-center gap-x-4">
|
<div className="flex flex-row items-center gap-x-4">
|
||||||
<div className="flex w-2/3 flex-col">
|
<div className="flex w-2/3 flex-col">
|
||||||
<Label>
|
<Label>
|
||||||
|
|||||||
@ -105,12 +105,8 @@ export const DropdownFieldAdvancedSettings = ({
|
|||||||
<Trans>Select default option</Trans>
|
<Trans>Select default option</Trans>
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={defaultValue}
|
defaultValue={defaultValue}
|
||||||
onValueChange={(val) => {
|
onValueChange={(val) => {
|
||||||
if (!val) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setDefaultValue(val);
|
setDefaultValue(val);
|
||||||
handleFieldChange('defaultValue', val);
|
handleFieldChange('defaultValue', val);
|
||||||
}}
|
}}
|
||||||
@ -176,7 +172,7 @@ export const DropdownFieldAdvancedSettings = ({
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="col-span-1 mt-auto inline-flex h-10 w-10 items-center text-slate-500 hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
|
className="col-span-1 mt-auto inline-flex h-10 w-10 items-center text-slate-500 hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
onClick={() => removeValue(index)}
|
onClick={() => removeValue(index)}
|
||||||
>
|
>
|
||||||
<Trash className="h-5 w-5" />
|
<Trash className="h-5 w-5" />
|
||||||
|
|||||||
@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
|
||||||
import { ChevronDown, ChevronUp, Trash } from 'lucide-react';
|
import { ChevronDown, ChevronUp, Trash } from 'lucide-react';
|
||||||
|
|
||||||
import { validateRadioField } from '@documenso/lib/advanced-fields-validation/validate-radio';
|
import { validateRadioField } from '@documenso/lib/advanced-fields-validation/validate-radio';
|
||||||
@ -28,8 +27,6 @@ export const RadioFieldAdvancedSettings = ({
|
|||||||
handleFieldChange,
|
handleFieldChange,
|
||||||
handleErrors,
|
handleErrors,
|
||||||
}: RadioFieldAdvancedSettingsProps) => {
|
}: RadioFieldAdvancedSettingsProps) => {
|
||||||
const { _ } = useLingui();
|
|
||||||
|
|
||||||
const [showValidation, setShowValidation] = useState(false);
|
const [showValidation, setShowValidation] = useState(false);
|
||||||
const [values, setValues] = useState(
|
const [values, setValues] = useState(
|
||||||
fieldState.values ?? [{ id: 1, checked: false, value: 'Default value' }],
|
fieldState.values ?? [{ id: 1, checked: false, value: 'Default value' }],
|
||||||
@ -105,18 +102,6 @@ export const RadioFieldAdvancedSettings = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div>
|
|
||||||
<Label>
|
|
||||||
<Trans>Label</Trans>
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="label"
|
|
||||||
className="bg-background mt-2"
|
|
||||||
placeholder={_(msg`Field label`)}
|
|
||||||
value={fieldState.label}
|
|
||||||
onChange={(e) => handleFieldChange('label', e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<Switch
|
<Switch
|
||||||
className="bg-background"
|
className="bg-background"
|
||||||
|
|||||||
@ -126,13 +126,7 @@ export const TextFieldAdvancedSettings = ({
|
|||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={fieldState.textAlign}
|
value={fieldState.textAlign}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => handleInput('textAlign', value)}
|
||||||
if (!value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleInput('textAlign', value);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<SelectTrigger className="bg-background mt-2">
|
<SelectTrigger className="bg-background mt-2">
|
||||||
<SelectValue placeholder="Select text align" />
|
<SelectValue placeholder="Select text align" />
|
||||||
|
|||||||
@ -183,12 +183,12 @@ const FormMessage = React.forwardRef<
|
|||||||
FormMessage.displayName = 'FormMessage';
|
FormMessage.displayName = 'FormMessage';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useFormField,
|
|
||||||
Form,
|
Form,
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
FormDescription,
|
||||||
FormMessage,
|
|
||||||
FormField,
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
useFormField,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -282,11 +282,7 @@ export const SignaturePad = ({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPressed) {
|
onMouseUp(event, false);
|
||||||
onMouseUp(event, true);
|
|
||||||
} else {
|
|
||||||
onMouseUp(event, false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClearClick = () => {
|
const onClearClick = () => {
|
||||||
|
|||||||
@ -1,83 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { ChevronDown } from 'lucide-react';
|
|
||||||
|
|
||||||
import { cn } from '../lib/utils';
|
|
||||||
import { Button } from './button';
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from './dropdown-menu';
|
|
||||||
|
|
||||||
const SplitButtonContext = React.createContext<{
|
|
||||||
variant?: React.ComponentProps<typeof Button>['variant'];
|
|
||||||
size?: React.ComponentProps<typeof Button>['size'];
|
|
||||||
}>({});
|
|
||||||
|
|
||||||
const SplitButton = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement> & {
|
|
||||||
variant?: React.ComponentProps<typeof Button>['variant'];
|
|
||||||
size?: React.ComponentProps<typeof Button>['size'];
|
|
||||||
}
|
|
||||||
>(({ className, children, variant = 'default', size = 'default', ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<SplitButtonContext.Provider value={{ variant, size }}>
|
|
||||||
<div ref={ref} className={cn('inline-flex', className)} {...props}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</SplitButtonContext.Provider>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
SplitButton.displayName = 'SplitButton';
|
|
||||||
|
|
||||||
const SplitButtonAction = React.forwardRef<
|
|
||||||
HTMLButtonElement,
|
|
||||||
React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
||||||
>(({ className, children, ...props }, ref) => {
|
|
||||||
const { variant, size } = React.useContext(SplitButtonContext);
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
ref={ref}
|
|
||||||
variant={variant}
|
|
||||||
size={size}
|
|
||||||
className={cn('rounded-r-none border-r-0', className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
SplitButtonAction.displayName = 'SplitButtonAction';
|
|
||||||
|
|
||||||
const SplitButtonDropdown = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
||||||
({ children, ...props }, ref) => {
|
|
||||||
const { variant, size } = React.useContext(SplitButtonContext);
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant={variant}
|
|
||||||
size={size}
|
|
||||||
className="rounded-l-none px-2 focus-visible:ring-offset-0"
|
|
||||||
>
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
<span className="sr-only">More options</span>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" {...props} ref={ref}>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
SplitButtonDropdown.displayName = 'SplitButtonDropdown';
|
|
||||||
|
|
||||||
const SplitButtonDropdownItem = DropdownMenuItem;
|
|
||||||
|
|
||||||
export { SplitButton, SplitButtonAction, SplitButtonDropdown, SplitButtonDropdownItem };
|
|
||||||
Reference in New Issue
Block a user