Compare commits

..

19 Commits

Author SHA1 Message Date
8a6942f9da chore: let user activily modify next signer 2025-02-19 00:08:45 +00:00
8b82d22f9f fix: build errors 2025-02-14 14:48:02 +00:00
00e402f4cb chore: cleanup 2025-02-14 13:05:18 +00:00
1e90ca45a6 chore: disable form on last signer 2025-02-14 12:48:05 +00:00
4189a34de0 feat: dictate next signers in signing ordeR 2025-02-14 10:32:18 +00:00
2ff330f9d4 chore: update local seed data (#1622)
## Description

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

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

## Checklist

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

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

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

View File

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

View File

@ -3,6 +3,8 @@ title: Public API
description: Learn how to interact with your documents programmatically using the Documenso public API. description: Learn how to interact with your documents programmatically using the Documenso public API.
--- ---
import { Callout, Steps } from 'nextra/components';
# Public API # Public API
Documenso provides a public REST API enabling you to interact with your documents programmatically. The API exposes various HTTP endpoints that allow you to perform operations such as: Documenso provides a public REST API enabling you to interact with your documents programmatically. The API exposes various HTTP endpoints that allow you to perform operations such as:
@ -13,10 +15,24 @@ Documenso provides a public REST API enabling you to interact with your document
The documentation walks you through creating API keys and using them to authenticate your API requests. You'll also learn about the available endpoints, request and response formats, and how to use the API. The documentation walks you through creating API keys and using them to authenticate your API requests. You'll also learn about the available endpoints, request and response formats, and how to use the API.
## Swagger Documentation ## API V1 - Stable
The [Swagger documentation](https://app.documenso.com/api/v1/openapi) also provides information about the API endpoints, request parameters, response formats, and authentication methods. Check out the [API V1 documentation](https://app.documenso.com/api/v1/openapi) for details about the API endpoints, request parameters, response formats, and authentication methods.
## API V2 - Beta
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.
<Callout type="warning">
NOW IN BETA: [API V2 Documentation](https://documen.so/api-v2-docs)
</Callout>
🚀 [V2 Announcement](https://documen.so/sdk-blog)
💬 [Leave Feedback](https://documen.so/sdk-feedback)
🔔 [Breaking Changes](https://documen.so/sdk-breaking)
## Availability ## Availability
The API is available to individual users and teams. The API is available to individual users, teams and higher plans. [Fair Use](https://documen.so/fair) applies.

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 10 KiB

View File

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

View File

@ -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 }) => {
@ -216,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({
@ -391,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}

View File

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

View File

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

View File

@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +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 { 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';
@ -21,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,
@ -34,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]);
@ -47,104 +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}
>
{isComplete ? <Trans>Complete</Trans> : <Trans>Next field</Trans>}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>
<div className="text-foreground text-xl font-semibold">
{role === RecipientRole.VIEWER && <Trans>Complete Viewing</Trans>}
{role === RecipientRole.SIGNER && <Trans>Complete Signing</Trans>}
{role === RecipientRole.APPROVER && <Trans>Complete Approval</Trans>}
</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 <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}
> >
{role === RecipientRole.VIEWER && <Trans>Mark as Viewed</Trans>} {isComplete ? <Trans>Complete</Trans> : <Trans>Next field</Trans>}
{role === RecipientRole.SIGNER && <Trans>Sign</Trans>}
{role === RecipientRole.APPROVER && <Trans>Approve</Trans>}
</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>
)}
</>
); );
}; }

View File

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

View File

@ -46,6 +46,7 @@ export type SignatureFieldProps = {
| 'Email' | 'Email'
| 'Name' | 'Name'
| 'Signature' | 'Signature'
| 'Text'
| 'Radio' | 'Radio'
| 'Dropdown' | 'Dropdown'
| 'Number' | 'Number'

View File

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

View File

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

View File

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

View File

@ -485,7 +485,6 @@ export const EmbedDirectTemplateClientPage = ({
{/* Fields */} {/* Fields */}
<EmbedDocumentFields <EmbedDocumentFields
recipient={recipient}
fields={localFields} fields={localFields}
metadata={metadata} metadata={metadata}
onSignField={onSignField} onSignField={onSignField}

View File

@ -13,6 +13,7 @@ import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-provider'; import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-provider';
import { SigningProvider } from '~/app/(signing)/sign/[token]/provider'; import { SigningProvider } from '~/app/(signing)/sign/[token]/provider';
import { RecipientProvider } from '~/app/(signing)/sign/[token]/recipient-context';
import { EmbedAuthenticateView } from '../../authenticate'; import { EmbedAuthenticateView } from '../../authenticate';
import { EmbedPaywall } from '../../paywall'; import { EmbedPaywall } from '../../paywall';
@ -96,16 +97,18 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem
recipient={recipient} recipient={recipient}
user={user} user={user}
> >
<EmbedDirectTemplateClientPage <RecipientProvider recipient={recipient}>
token={token} <EmbedDirectTemplateClientPage
updatedAt={template.updatedAt} token={token}
documentData={template.templateDocumentData} updatedAt={template.updatedAt}
recipient={recipient} documentData={template.templateDocumentData}
fields={fields} recipient={recipient}
metadata={template.templateMeta} fields={fields}
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy} metadata={template.templateMeta}
isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument} hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
/> isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument}
/>
</RecipientProvider>
</DocumentAuthProvider> </DocumentAuthProvider>
</SigningProvider> </SigningProvider>
); );

View File

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

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useEffect, useLayoutEffect, 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';
@ -9,8 +9,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 { validateFieldsInserted } from '@documenso/lib/utils/fields'; import { validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client'; import type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client';
import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client'; import { type DocumentData, type Field, FieldType, RecipientRole } from '@documenso/prisma/client';
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip'; import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -19,10 +20,12 @@ import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label'; import { Label } from '@documenso/ui/primitives/label';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast'; 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 { Logo } from '~/components/branding/logo'; import { Logo } from '~/components/branding/logo';
import { EmbedClientLoading } from '../../client-loading'; import { EmbedClientLoading } from '../../client-loading';
@ -35,12 +38,13 @@ export type EmbedSignDocumentClientPageProps = {
token: string; token: string;
documentId: number; documentId: number;
documentData: DocumentData; documentData: DocumentData;
recipient: Recipient; recipient: RecipientWithFields;
fields: Field[]; fields: Field[];
metadata?: DocumentMeta | TemplateMeta | null; metadata?: DocumentMeta | TemplateMeta | null;
isCompleted?: boolean; isCompleted?: boolean;
hidePoweredBy?: boolean; hidePoweredBy?: boolean;
isPlatformOrEnterprise?: boolean; isPlatformOrEnterprise?: boolean;
allRecipients?: RecipientWithFields[];
}; };
export const EmbedSignDocumentClientPage = ({ export const EmbedSignDocumentClientPage = ({
@ -53,6 +57,7 @@ export const EmbedSignDocumentClientPage = ({
isCompleted, isCompleted,
hidePoweredBy = false, hidePoweredBy = false,
isPlatformOrEnterprise = false, isPlatformOrEnterprise = false,
allRecipients = [],
}: EmbedSignDocumentClientPageProps) => { }: EmbedSignDocumentClientPageProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
@ -70,17 +75,21 @@ 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 [selectedSignerId, setSelectedSignerId] = useState<number | null>(
allRecipients.length > 0 ? allRecipients[0].id : null,
);
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
const [isNameLocked, setIsNameLocked] = useState(false); const [isNameLocked, setIsNameLocked] = useState(false);
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false); const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId);
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((field) => !field.inserted), fields.filter((field) => field.recipientId === recipient.id && !field.inserted),
fields.filter((field) => field.inserted), fields.filter((field) => field.inserted),
]; ];
@ -89,6 +98,8 @@ export const EmbedSignDocumentClientPage = ({
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE); const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
const assistantSignersId = useId();
const onNextFieldClick = () => { const onNextFieldClick = () => {
validateFieldsInserted(fields); validateFieldsInserted(fields);
@ -214,164 +225,234 @@ export const EmbedSignDocumentClientPage = ({
} }
return ( return (
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6"> <RecipientProvider recipient={recipient} targetSigner={selectedSigner ?? null}>
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />} <div className="embed--Root relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
<div className="relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row"> <div className="embed--DocumentContainer relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
{/* Viewer */} {/* Viewer */}
<div className="flex-1"> <div className="embed--DocumentViewer flex-1">
<LazyPDFViewer <LazyPDFViewer
documentData={documentData} documentData={documentData}
onDocumentLoad={() => setHasDocumentLoaded(true)} onDocumentLoad={() => setHasDocumentLoaded(true)}
/> />
</div> </div>
{/* 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 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="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">
{/* Header */} {/* Header */}
<div> <div className="embed--DocumentWidgetHeader">
<div className="flex items-center justify-between gap-x-2"> <div className="flex items-center justify-between gap-x-2">
<h3 className="text-foreground text-xl font-semibold md:text-2xl"> <h3 className="text-foreground text-xl font-semibold md:text-2xl">
<Trans>Sign document</Trans> {isAssistantMode ? (
</h3> <Trans>Assist with signing</Trans>
) : (
<Trans>Sign document</Trans>
)}
</h3>
<Button variant="outline" className="h-8 w-8 p-0 md:hidden"> <Button variant="outline" className="h-8 w-8 p-0 md:hidden">
{isExpanded ? ( {isExpanded ? (
<LucideChevronDown <LucideChevronDown
className="text-muted-foreground h-5 w-5" className="text-muted-foreground h-5 w-5"
onClick={() => setIsExpanded(false)} onClick={() => setIsExpanded(false)}
/>
) : (
<LucideChevronUp
className="text-muted-foreground h-5 w-5"
onClick={() => setIsExpanded(true)}
/>
)}
</Button>
</div>
</div>
<div className="hidden group-data-[expanded]/document-widget:block md:block">
<p className="text-muted-foreground mt-2 text-sm">
<Trans>Sign the document to complete the process.</Trans>
</p>
<hr className="border-border mb-8 mt-4" />
</div>
{/* Form */}
<div className="-mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
<div className="flex flex-1 flex-col gap-y-4">
<div>
<Label htmlFor="full-name">
<Trans>Full Name</Trans>
</Label>
<Input
type="text"
id="full-name"
className="bg-background mt-2"
disabled={isNameLocked}
value={fullName}
onChange={(e) => !isNameLocked && setFullName(e.target.value)}
/>
</div>
<div>
<Label htmlFor="email">
<Trans>Email</Trans>
</Label>
<Input
type="email"
id="email"
className="bg-background mt-2"
value={email}
disabled
/>
</div>
<div>
<Label htmlFor="Signature">
<Trans>Signature</Trans>
</Label>
<Card className="mt-2" gradient degrees={-120}>
<CardContent className="p-0">
<SignaturePad
className="h-44 w-full"
disabled={isThrottled || isSubmitting}
defaultValue={signature ?? undefined}
onChange={(value) => {
setSignature(value);
}}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
allowTypedSignature={Boolean(
metadata &&
'typedSignatureEnabled' in metadata &&
metadata.typedSignatureEnabled,
)}
/> />
</CardContent> ) : (
</Card> <LucideChevronUp
className="text-muted-foreground h-5 w-5"
onClick={() => setIsExpanded(true)}
/>
)}
</Button>
</div>
</div>
{hasSignatureField && !signatureValid && ( <div className="embed--DocumentWidgetContent hidden group-data-[expanded]/document-widget:block md:block">
<div className="text-destructive mt-2 text-sm"> <p className="text-muted-foreground mt-2 text-sm">
<Trans> {isAssistantMode ? (
Signature is too small. Please provide a more complete signature. <Trans>Help complete the document for other signers.</Trans>
</Trans> ) : (
<Trans>Sign the document to complete the process.</Trans>
)}
</p>
<hr className="border-border mb-8 mt-4" />
</div>
{/* Form */}
<div className="embed--DocumentWidgetForm -mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
<div className="flex flex-1 flex-col gap-y-4">
{isAssistantMode && (
<div>
<Label>
<Trans>Signing for</Trans>
</Label>
<fieldset className="dark:bg-background border-border mt-2 rounded-2xl border bg-white p-3">
<RadioGroup
className="gap-0 space-y-3 shadow-none"
value={selectedSignerId?.toString()}
onValueChange={(value) => setSelectedSignerId(Number(value))}
>
{allRecipients
.filter((r) => r.fields.length > 0)
.map((r) => (
<div
key={`${assistantSignersId}-${r.id}`}
className="bg-widget border-border relative flex flex-col gap-4 rounded-lg border p-4"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<RadioGroupItem
id={`${assistantSignersId}-${r.id}`}
value={r.id.toString()}
className="after:absolute after:inset-0"
/>
<div className="grid grow gap-1">
<Label
className="inline-flex items-start"
htmlFor={`${assistantSignersId}-${r.id}`}
>
{r.name}
{r.id === recipient.id && (
<span className="text-muted-foreground ml-2">
{_(msg`(You)`)}
</span>
)}
</Label>
<p className="text-muted-foreground text-xs">{r.email}</p>
</div>
</div>
<div className="text-muted-foreground text-xs leading-[inherit]">
{r.fields.length} {r.fields.length === 1 ? 'field' : 'fields'}
</div>
</div>
</div>
))}
</RadioGroup>
</fieldset>
</div> </div>
)} )}
{!isAssistantMode && (
<>
<div>
<Label htmlFor="full-name">
<Trans>Full Name</Trans>
</Label>
<Input
type="text"
id="full-name"
className="bg-background mt-2"
disabled={isNameLocked}
value={fullName}
onChange={(e) => !isNameLocked && setFullName(e.target.value)}
/>
</div>
<div>
<Label htmlFor="email">
<Trans>Email</Trans>
</Label>
<Input
type="email"
id="email"
className="bg-background mt-2"
value={email}
disabled
/>
</div>
<div>
<Label htmlFor="Signature">
<Trans>Signature</Trans>
</Label>
<Card className="mt-2" gradient degrees={-120}>
<CardContent className="p-0">
<SignaturePad
className="h-44 w-full"
disabled={isThrottled || isSubmitting}
defaultValue={signature ?? undefined}
onChange={(value) => {
setSignature(value);
}}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
allowTypedSignature={Boolean(
metadata &&
'typedSignatureEnabled' in metadata &&
metadata.typedSignatureEnabled,
)}
/>
</CardContent>
</Card>
{hasSignatureField && !signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>
Signature is too small. Please provide a more complete signature.
</Trans>
</div>
)}
</div>
</>
)}
</div> </div>
</div> </div>
</div>
<div className="hidden flex-1 group-data-[expanded]/document-widget:block md:block" /> <div className="hidden flex-1 group-data-[expanded]/document-widget:block md:block" />
<div className="mt-4 hidden w-full grid-cols-2 items-center group-data-[expanded]/document-widget:grid md:grid"> <div className="embed--DocumentWidgetFooter mt-4 hidden w-full grid-cols-2 items-center group-data-[expanded]/document-widget:grid md:grid">
{pendingFields.length > 0 ? ( {pendingFields.length > 0 ? (
<Button className="col-start-2" onClick={() => onNextFieldClick()}> <Button className="col-start-2" onClick={() => onNextFieldClick()}>
<Trans>Next</Trans> <Trans>Next</Trans>
</Button> </Button>
) : ( ) : (
<Button <Button
className="col-start-2" className="col-start-2"
disabled={isThrottled || (hasSignatureField && !signatureValid)} disabled={
loading={isSubmitting} isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
onClick={() => throttledOnCompleteClick()} }
> loading={isSubmitting}
<Trans>Complete</Trans> onClick={() => throttledOnCompleteClick()}
</Button> >
)} <Trans>Complete</Trans>
</Button>
)}
</div>
</div> </div>
</div> </div>
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
{showPendingFieldTooltip && pendingFields.length > 0 && (
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning">
<Trans>Click to insert field</Trans>
</FieldToolTip>
)}
</ElementVisible>
{/* Fields */}
<EmbedDocumentFields fields={fields} metadata={metadata} />
</div> </div>
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}> {!hidePoweredBy && (
{showPendingFieldTooltip && pendingFields.length > 0 && ( <div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning"> <span>Powered by</span>
<Trans>Click to insert field</Trans> <Logo className="ml-2 inline-block h-[14px]" />
</FieldToolTip> </div>
)} )}
</ElementVisible>
{/* Fields */}
<EmbedDocumentFields recipient={recipient} fields={fields} metadata={metadata} />
</div> </div>
</RecipientProvider>
{!hidePoweredBy && (
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
<span>Powered by</span>
<Logo className="ml-2 inline-block h-[14px]" />
</div>
)}
</div>
); );
}; };

View File

@ -8,17 +8,20 @@ import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token'; import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token'; import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getRecipientsForAssistant } from '@documenso/lib/server-only/recipient/get-recipients-for-assistant';
import { getTeamById } from '@documenso/lib/server-only/team/get-team'; import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth'; import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth'; import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { DocumentStatus } from '@documenso/prisma/client'; import { DocumentStatus, RecipientRole } from '@documenso/prisma/client';
import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-provider'; import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-provider';
import { SigningProvider } from '~/app/(signing)/sign/[token]/provider'; import { SigningProvider } from '~/app/(signing)/sign/[token]/provider';
import { EmbedAuthenticateView } from '../../authenticate'; import { EmbedAuthenticateView } from '../../authenticate';
import { EmbedPaywall } from '../../paywall'; import { EmbedPaywall } from '../../paywall';
import { EmbedWaitingForTurn } from '../../waiting-for-turn';
import { EmbedSignDocumentClientPage } from './client'; import { EmbedSignDocumentClientPage } from './client';
export type EmbedSignDocumentPageProps = { export type EmbedSignDocumentPageProps = {
@ -85,6 +88,19 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
); );
} }
const isRecipientsTurnToSign = await getIsRecipientsTurnToSign({ token });
if (!isRecipientsTurnToSign) {
return <EmbedWaitingForTurn />;
}
const allRecipients =
recipient.role === RecipientRole.ASSISTANT
? await getRecipientsForAssistant({
token,
})
: [];
const team = document.teamId const team = document.teamId
? await getTeamById({ teamId: document.teamId, userId: document.userId }).catch(() => null) ? await getTeamById({ teamId: document.teamId, userId: document.userId }).catch(() => null)
: null; : null;
@ -112,6 +128,7 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
isCompleted={document.status === DocumentStatus.COMPLETED} isCompleted={document.status === DocumentStatus.COMPLETED}
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy} hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument} isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument}
allRecipients={allRecipients}
/> />
</DocumentAuthProvider> </DocumentAuthProvider>
</SigningProvider> </SigningProvider>

View File

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

View File

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

View File

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

View File

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

24
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "@documenso/root", "name": "@documenso/root",
"version": "1.9.0-rc.11", "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.0-rc.11", "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.0-rc.11", "version": "1.9.1-rc.1",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@documenso/api": "*", "@documenso/api": "*",
@ -32565,8 +32565,7 @@
"node_modules/uqr": { "node_modules/uqr": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.2.tgz", "resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.2.tgz",
"integrity": "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==", "integrity": "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA=="
"license": "MIT"
}, },
"node_modules/uri-js": { "node_modules/uri-js": {
"version": "4.4.1", "version": "4.4.1",
@ -35723,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"
}
} }
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"private": true, "private": true,
"version": "1.9.0-rc.11", "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",

View File

@ -540,12 +540,19 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip
if (i > 1) { if (i > 1) {
await page.getByRole('button', { name: 'Add Signer' }).click(); await page.getByRole('button', { name: 'Add Signer' }).click();
} }
await page await page
.getByPlaceholder('Email') .getByLabel('Email')
.nth(i - 1)
.focus();
await page
.getByLabel('Email')
.nth(i - 1) .nth(i - 1)
.fill(`user${i}@example.com`); .fill(`user${i}@example.com`);
await page await page
.getByPlaceholder('Name') .getByLabel('Name')
.nth(i - 1) .nth(i - 1)
.fill(`User ${i}`); .fill(`User ${i}`);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
}, },
}); });

View File

@ -28,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) => {
@ -51,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 });
@ -112,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: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: fr\n" "Language: fr\n"
"Project-Id-Version: documenso-app\n" "Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-01-30 03:57\n" "PO-Revision-Date: 2025-01-30 06:04\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: French\n" "Language-Team: French\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n"
@ -163,7 +163,7 @@ msgstr "{inviterName} vous a retiré du document<0/>\"{documentName}\""
#: packages/email/template-components/template-document-invite.tsx:61 #: packages/email/template-components/template-document-invite.tsx:61
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {0}<0/>\"{documentName}\"" msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {0}<0/>\"{documentName}\""
msgstr "" msgstr "{inviterName} représentant \"{teamName}\" vous a invité à {0}<0/>\"{documentName}\""
#: packages/email/templates/document-invite.tsx:45 #: packages/email/templates/document-invite.tsx:45
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {action} {documentName}" msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {action} {documentName}"
@ -308,7 +308,7 @@ msgstr "{signerName} a rejeté le document \"{documentName}\"."
#: packages/email/template-components/template-document-invite.tsx:68 #: packages/email/template-components/template-document-invite.tsx:68
msgid "{teamName} has invited you to {0}<0/>\"{documentName}\"" msgid "{teamName} has invited you to {0}<0/>\"{documentName}\""
msgstr "" msgstr "{teamName} vous a invité à {0}<0/>\"{documentName}\""
#: packages/email/templates/document-invite.tsx:46 #: packages/email/templates/document-invite.tsx:46
msgid "{teamName} has invited you to {action} {documentName}" msgid "{teamName} has invited you to {action} {documentName}"
@ -1232,20 +1232,20 @@ msgstr "Importation en masse"
#: packages/lib/jobs/definitions/internal/bulk-send-template.handler.ts:203 #: packages/lib/jobs/definitions/internal/bulk-send-template.handler.ts:203
msgid "Bulk Send Complete: {0}" msgid "Bulk Send Complete: {0}"
msgstr "" msgstr "Envoi en masse terminé : {0}"
#: packages/email/templates/bulk-send-complete.tsx:30 #: packages/email/templates/bulk-send-complete.tsx:30
msgid "Bulk send operation complete for template \"{templateName}\"" msgid "Bulk send operation complete for template \"{templateName}\""
msgstr "" msgstr "Envoi groupé terminé pour le modèle \"{templateName}\""
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:128 #: apps/web/src/components/templates/template-bulk-send-dialog.tsx:128
msgid "Bulk Send Template via CSV" msgid "Bulk Send Template via CSV"
msgstr "" msgstr "Envoi de modèle groupé via CSV"
#: apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx:97 #: apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx:97
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:120 #: apps/web/src/components/templates/template-bulk-send-dialog.tsx:120
msgid "Bulk Send via CSV" msgid "Bulk Send via CSV"
msgstr "" msgstr "Envoi en masse via CSV"
#: packages/email/templates/team-invite.tsx:84 #: packages/email/templates/team-invite.tsx:84
msgid "by <0>{senderName}</0>" msgid "by <0>{senderName}</0>"
@ -1788,7 +1788,7 @@ msgstr "Créé le {0}"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:143 #: apps/web/src/components/templates/template-bulk-send-dialog.tsx:143
msgid "CSV Structure" msgid "CSV Structure"
msgstr "" msgstr "Structure CSV"
#: apps/web/src/components/forms/password.tsx:112 #: apps/web/src/components/forms/password.tsx:112
msgid "Current Password" msgid "Current Password"
@ -1800,7 +1800,7 @@ msgstr "Le mot de passe actuel est incorrect."
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:154 #: apps/web/src/components/templates/template-bulk-send-dialog.tsx:154
msgid "Current recipients:" msgid "Current recipients:"
msgstr "" msgstr "Destinataires actuels :"
#: apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx:28 #: apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx:28
msgid "Daily" msgid "Daily"
@ -2328,7 +2328,7 @@ msgstr "Télécharger le certificat"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:168 #: apps/web/src/components/templates/template-bulk-send-dialog.tsx:168
msgid "Download Template CSV" msgid "Download Template CSV"
msgstr "" msgstr "Télécharger le modèle CSV"
#: apps/web/src/app/(dashboard)/templates/[id]/template-page-view-documents-table.tsx:208 #: apps/web/src/app/(dashboard)/templates/[id]/template-page-view-documents-table.tsx:208
#: apps/web/src/components/formatter/document-status.tsx:34 #: apps/web/src/components/formatter/document-status.tsx:34
@ -2659,7 +2659,7 @@ msgstr "Échec de la mise à jour du webhook"
#: packages/email/templates/bulk-send-complete.tsx:55 #: packages/email/templates/bulk-send-complete.tsx:55
msgid "Failed: {failedCount}" msgid "Failed: {failedCount}"
msgstr "" msgstr "Échoués : {failedCount}"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:100 #: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:100
msgid "Field character limit" msgid "Field character limit"
@ -2721,7 +2721,7 @@ msgstr "Pour toute question concernant cette divulgation, les signatures électr
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:147 #: 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 "" 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?"
@ -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 "" 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>"
@ -3238,7 +3238,7 @@ 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 "" 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"
@ -3868,7 +3868,7 @@ msgstr "Veuillez taper <0>{0}</0> pour confirmer."
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:172 #: apps/web/src/components/templates/template-bulk-send-dialog.tsx:172
msgid "Pre-formatted CSV template with example data." msgid "Pre-formatted CSV template with example data."
msgstr "" msgstr "Modèle CSV pré-formaté avec des données d'exemple."
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:214 #: apps/web/src/components/(dashboard)/common/command-menu.tsx:214
#: apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx:58 #: apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx:58
@ -4392,7 +4392,7 @@ msgstr "Envoyer des documents au nom de l'équipe en utilisant l'adresse e-mail"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:253 #: apps/web/src/components/templates/template-bulk-send-dialog.tsx:253
msgid "Send documents to recipients immediately" msgid "Send documents to recipients immediately"
msgstr "" msgstr "Envoyer les documents aux destinataires immédiatement"
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:200 #: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:200
msgid "Send on Behalf of Team" msgid "Send on Behalf of Team"
@ -4864,11 +4864,11 @@ msgstr "Clé d'authentification créée avec succès"
#: packages/email/templates/bulk-send-complete.tsx:52 #: packages/email/templates/bulk-send-complete.tsx:52
msgid "Successfully created: {successCount}" msgid "Successfully created: {successCount}"
msgstr "" msgstr "Créés avec succès : {successCount}"
#: packages/email/templates/bulk-send-complete.tsx:44 #: packages/email/templates/bulk-send-complete.tsx:44
msgid "Summary:" msgid "Summary:"
msgstr "" msgstr "Résumé :"
#: apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx:57 #: apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx:57
msgid "System Requirements" msgid "System Requirements"
@ -5089,7 +5089,7 @@ msgstr "Texte"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx:140 #: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx:140
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:124 #: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:124
msgid "Text Align" msgid "Text Align"
msgstr "" msgstr "Alignement du texte"
#: apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx:157 #: apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx:157
msgid "Text Color" msgid "Text Color"
@ -5177,7 +5177,7 @@ msgstr "Les événements qui déclencheront un webhook à envoyer à votre URL."
#: packages/email/templates/bulk-send-complete.tsx:62 #: packages/email/templates/bulk-send-complete.tsx:62
msgid "The following errors occurred:" msgid "The following errors occurred:"
msgstr "" msgstr "Les erreurs suivantes se sont produites :"
#: packages/email/templates/team-delete.tsx:37 #: packages/email/templates/team-delete.tsx:37
msgid "The following team has been deleted by its owner. You will no longer be able to access this team and its documents" msgid "The following team has been deleted by its owner. You will no longer be able to access this team and its documents"
@ -5637,7 +5637,7 @@ msgstr "Total des destinataires"
#: packages/email/templates/bulk-send-complete.tsx:49 #: packages/email/templates/bulk-send-complete.tsx:49
msgid "Total rows processed: {totalProcessed}" msgid "Total rows processed: {totalProcessed}"
msgstr "" msgstr "Lignes totales traitées : {totalProcessed}"
#: apps/web/src/app/(dashboard)/admin/stats/page.tsx:150 #: apps/web/src/app/(dashboard)/admin/stats/page.tsx:150
msgid "Total Signers that Signed Up" msgid "Total Signers that Signed Up"
@ -5899,7 +5899,7 @@ 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 "" 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"
@ -5907,7 +5907,7 @@ msgstr "Téléchargez un document personnalisé à utiliser à la place du docum
#: 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 "" 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"
@ -5915,7 +5915,7 @@ 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 "" 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"
@ -6594,7 +6594,7 @@ msgstr "Vous pouvez voir les documents associés à cet e-mail et utiliser cette
#: packages/email/templates/bulk-send-complete.tsx:76 #: packages/email/templates/bulk-send-complete.tsx:76
msgid "You can view the created documents in your dashboard under the \"Documents created from template\" section." msgid "You can view the created documents in your dashboard under the \"Documents created from template\" section."
msgstr "" msgstr "Vous pouvez voir les documents créés dans votre tableau de bord sous la section \"Documents créés à partir du modèle\"."
#: packages/email/template-components/template-document-rejected.tsx:37 #: packages/email/template-components/template-document-rejected.tsx:37
msgid "You can view the document and its status by clicking the button below." msgid "You can view the document and its status by clicking the button below."
@ -6807,11 +6807,11 @@ msgstr "Vos préférences de branding ont été mises à jour"
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:97 #: apps/web/src/components/templates/template-bulk-send-dialog.tsx:97
msgid "Your bulk send has been initiated. You will receive an email notification upon completion." msgid "Your bulk send has been initiated. You will receive an email notification upon completion."
msgstr "" msgstr "Votre envoi groupé a été initié. Vous recevrez une notification par email une fois terminé."
#: packages/email/templates/bulk-send-complete.tsx:40 #: 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 "" 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."
@ -6962,3 +6962,4 @@ msgstr "Votre token a été créé avec succès ! Assurez-vous de le copier car
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx:86 #: apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx:86
msgid "Your tokens will be shown here once you create them." msgid "Your tokens will be shown here once you create them."
msgstr "Vos tokens seront affichés ici une fois que vous les aurez créés." msgstr "Vos tokens seront affichés ici une fois que vous les aurez créés."

View File

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

View File

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

View File

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

View File

@ -55,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(),

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "DocumentMeta" ADD COLUMN "modifyNextSigner" BOOLEAN NOT NULL DEFAULT false;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "TemplateMeta" ADD COLUMN "modifyNextSigner" BOOLEAN NOT NULL DEFAULT false;

View File

@ -329,8 +329,7 @@ model Document {
template Template? @relation(fields: [templateId], references: [id], onDelete: SetNull) template Template? @relation(fields: [templateId], references: [id], onDelete: SetNull)
source DocumentSource source DocumentSource
auditLogs DocumentAuditLog[] auditLogs DocumentAuditLog[]
documentAccessToken DocumentAccessToken?
@@unique([documentDataId]) @@unique([documentDataId])
@@index([userId]) @@index([userId])
@ -391,29 +390,13 @@ 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)
emailSettings Json? /// [DocumentEmailSettings] @zod.custom.use(ZDocumentEmailSettingsSchema) emailSettings Json? /// [DocumentEmailSettings] @zod.custom.use(ZDocumentEmailSettingsSchema)
} }
model DocumentAccessToken {
id String @id @default(cuid())
token String @unique
documentId Int @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
expiresAt DateTime?
revokedAt DateTime?
lastAccessedAt DateTime?
accessCount Int @default(0)
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
@@index([token])
@@index([documentId])
}
enum ReadStatus { enum ReadStatus {
NOT_OPENED NOT_OPENED
OPENED OPENED
@ -435,6 +418,7 @@ enum RecipientRole {
SIGNER SIGNER
VIEWER VIEWER
APPROVER APPROVER
ASSISTANT
} }
/// @zod.import(["import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';"]) /// @zod.import(["import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';"])
@ -676,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)

View File

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

View File

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

View File

@ -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,
}); });
} }

View File

@ -251,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(),
}); });

View File

@ -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),
}); });
}), }),

View File

@ -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<

View File

@ -31,7 +31,8 @@ const getCardClassNames = (
checkBoxOrRadio: boolean, checkBoxOrRadio: boolean,
cardClassName?: string, cardClassName?: string,
) => { ) => {
const baseClasses = 'field-card-container relative z-20 h-full w-full transition-all'; const baseClasses =
'field--FieldRootContainer field-card-container relative z-20 h-full w-full transition-all';
const insertedClasses = const insertedClasses =
'bg-primary/20 border-primary ring-primary/20 ring-offset-primary/20 ring-2 ring-offset-2 dark:shadow-none'; 'bg-primary/20 border-primary ring-primary/20 ring-offset-primary/20 ring-2 ring-offset-2 dark:shadow-none';
@ -141,6 +142,7 @@ export function FieldRootContainer({ field, children, cardClassName }: FieldCont
<Card <Card
id={`field-${field.id}`} id={`field-${field.id}`}
ref={ref} ref={ref}
data-field-type={field.type}
data-inserted={field.inserted ? 'true' : 'false'} data-inserted={field.inserted ? 'true' : 'false'}
className={cardClassNames} className={cardClassNames}
> >

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { forwardRef } from 'react'; import { forwardRef } from 'react';
import { Trans } from '@lingui/macro'; import { Trans } from '@lingui/macro';
import type { SelectProps } from '@radix-ui/react-select'; import type { SelectProps } from '@radix-ui/react-select';
@ -11,12 +11,15 @@ import { ROLE_ICONS } from '@documenso/ui/primitives/recipient-role-icons';
import { Select, SelectContent, SelectItem, SelectTrigger } from '@documenso/ui/primitives/select'; import { Select, SelectContent, SelectItem, SelectTrigger } from '@documenso/ui/primitives/select';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { cn } from '../../lib/utils';
export type RecipientRoleSelectProps = SelectProps & { export type RecipientRoleSelectProps = SelectProps & {
hideCCRecipients?: boolean; hideCCRecipients?: boolean;
isAssistantEnabled?: boolean;
}; };
export const RecipientRoleSelect = forwardRef<HTMLButtonElement, RecipientRoleSelectProps>( export const RecipientRoleSelect = forwardRef<HTMLButtonElement, RecipientRoleSelectProps>(
({ hideCCRecipients, ...props }, ref) => ( ({ hideCCRecipients, isAssistantEnabled = true, ...props }, ref) => (
<Select {...props}> <Select {...props}>
<SelectTrigger ref={ref} className="bg-background w-[50px] p-2"> <SelectTrigger ref={ref} className="bg-background w-[50px] p-2">
{/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */} {/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */}
@ -110,6 +113,42 @@ export const RecipientRoleSelect = forwardRef<HTMLButtonElement, RecipientRoleSe
</div> </div>
</SelectItem> </SelectItem>
)} )}
<SelectItem
value={RecipientRole.ASSISTANT}
disabled={!isAssistantEnabled}
className={cn(
!isAssistantEnabled &&
'cursor-not-allowed opacity-50 data-[disabled]:pointer-events-auto',
)}
>
<div className="flex items-center">
<div className="flex w-[150px] items-center">
<span className="mr-2">{ROLE_ICONS[RecipientRole.ASSISTANT]}</span>
<Trans>Can prepare</Trans>
</div>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
<p>
{isAssistantEnabled ? (
<Trans>
The recipient can prepare the document for later signers by pre-filling
suggest values.
</Trans>
) : (
<Trans>
Assistant role is only available when the document is in sequential signing
mode.
</Trans>
)}
</p>
</TooltipContent>
</Tooltip>
</div>
</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
), ),

View File

@ -508,7 +508,15 @@ export const AddFieldsFormPartial = ({
}, []); }, []);
useEffect(() => { useEffect(() => {
setSelectedSigner(recipients.find((r) => r.sendStatus !== SendStatus.SENT) ?? recipients[0]); const recipientsByRoleToDisplay = recipients.filter(
(recipient) =>
recipient.role !== RecipientRole.CC && recipient.role !== RecipientRole.ASSISTANT,
);
setSelectedSigner(
recipientsByRoleToDisplay.find((r) => r.sendStatus !== SendStatus.SENT) ??
recipientsByRoleToDisplay[0],
);
}, [recipients]); }, [recipients]);
const recipientsByRole = useMemo(() => { const recipientsByRole = useMemo(() => {
@ -517,6 +525,7 @@ export const AddFieldsFormPartial = ({
VIEWER: [], VIEWER: [],
SIGNER: [], SIGNER: [],
APPROVER: [], APPROVER: [],
ASSISTANT: [],
}; };
recipients.forEach((recipient) => { recipients.forEach((recipient) => {
@ -529,7 +538,12 @@ export const AddFieldsFormPartial = ({
const recipientsByRoleToDisplay = useMemo(() => { const recipientsByRoleToDisplay = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][]) return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][])
.filter(([role]) => role !== RecipientRole.CC && role !== RecipientRole.VIEWER) .filter(
([role]) =>
role !== RecipientRole.CC &&
role !== RecipientRole.VIEWER &&
role !== RecipientRole.ASSISTANT,
)
.map( .map(
([role, roleRecipients]) => ([role, roleRecipients]) =>
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@ -544,12 +558,6 @@ export const AddFieldsFormPartial = ({
); );
}, [recipientsByRole]); }, [recipientsByRole]);
const isTypedSignatureEnabled = form.watch('typedSignatureEnabled');
const handleTypedSignatureChange = (value: boolean) => {
form.setValue('typedSignatureEnabled', value, { shouldDirty: true });
};
const handleAdvancedSettings = () => { const handleAdvancedSettings = () => {
setShowAdvancedSettings((prev) => !prev); setShowAdvancedSettings((prev) => !prev);
}; };
@ -687,9 +695,7 @@ export const AddFieldsFormPartial = ({
)} )}
{!selectedSigner?.email && ( {!selectedSigner?.email && (
<span className="gradie flex-1 truncate text-left"> <span className="flex-1 truncate text-left">{selectedSigner?.email}</span>
{selectedSigner?.email}
</span>
)} )}
<ChevronsUpDown className="ml-2 h-4 w-4" /> <ChevronsUpDown className="ml-2 h-4 w-4" />

View File

@ -41,6 +41,7 @@ import {
DocumentFlowFormContainerStep, DocumentFlowFormContainerStep,
} from './document-flow-root'; } from './document-flow-root';
import { ShowFieldItem } from './show-field-item'; import { ShowFieldItem } from './show-field-item';
import { SigningOrderConfirmation } from './signing-order-confirmation';
import type { DocumentFlowStep } from './types'; import type { DocumentFlowStep } from './types';
export type AddSignersFormProps = { export type AddSignersFormProps = {
@ -48,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;
@ -58,6 +60,7 @@ export const AddSignersFormPartial = ({
recipients, recipients,
fields, fields,
signingOrder, signingOrder,
modifyNextSigner,
isDocumentEnterprise, isDocumentEnterprise,
onSubmit, onSubmit,
isDocumentPdfLoaded, isDocumentPdfLoaded,
@ -106,6 +109,7 @@ export const AddSignersFormPartial = ({
) )
: defaultRecipients, : defaultRecipients,
signingOrder: signingOrder || DocumentSigningOrder.PARALLEL, signingOrder: signingOrder || DocumentSigningOrder.PARALLEL,
modifyNextSigner: modifyNextSigner ?? false,
}, },
}); });
@ -123,6 +127,7 @@ export const AddSignersFormPartial = ({
}, [recipients, form]); }, [recipients, form]);
const [showAdvancedSettings, setShowAdvancedSettings] = useState(alwaysShowAdvancedSettings); const [showAdvancedSettings, setShowAdvancedSettings] = useState(alwaysShowAdvancedSettings);
const [showSigningOrderConfirmation, setShowSigningOrderConfirmation] = useState(false);
const { const {
setValue, setValue,
@ -134,6 +139,10 @@ export const AddSignersFormPartial = ({
const watchedSigners = watch('signers'); const watchedSigners = watch('signers');
const isSigningOrderSequential = watch('signingOrder') === DocumentSigningOrder.SEQUENTIAL; const isSigningOrderSequential = watch('signingOrder') === DocumentSigningOrder.SEQUENTIAL;
const hasAssistantRole = useMemo(() => {
return watchedSigners.some((signer) => signer.role === RecipientRole.ASSISTANT);
}, [watchedSigners]);
const normalizeSigningOrders = (signers: typeof watchedSigners) => { const normalizeSigningOrders = (signers: typeof watchedSigners) => {
return signers return signers
.sort((a, b) => (a.signingOrder ?? 0) - (b.signingOrder ?? 0)) .sort((a, b) => (a.signingOrder ?? 0) - (b.signingOrder ?? 0))
@ -233,6 +242,7 @@ export const AddSignersFormPartial = ({
const items = Array.from(watchedSigners); const items = Array.from(watchedSigners);
const [reorderedSigner] = items.splice(result.source.index, 1); const [reorderedSigner] = items.splice(result.source.index, 1);
// Find next valid position
let insertIndex = result.destination.index; let insertIndex = result.destination.index;
while (insertIndex < items.length && !canRecipientBeModified(items[insertIndex].nativeId)) { while (insertIndex < items.length && !canRecipientBeModified(items[insertIndex].nativeId)) {
insertIndex++; insertIndex++;
@ -240,126 +250,116 @@ export const AddSignersFormPartial = ({
items.splice(insertIndex, 0, reorderedSigner); items.splice(insertIndex, 0, reorderedSigner);
const updatedSigners = items.map((item, index) => ({ const updatedSigners = items.map((signer, index) => ({
...item, ...signer,
signingOrder: !canRecipientBeModified(item.nativeId) ? item.signingOrder : index + 1, signingOrder: !canRecipientBeModified(signer.nativeId) ? signer.signingOrder : index + 1,
})); }));
updatedSigners.forEach((item, index) => { form.setValue('signers', updatedSigners);
const keys: (keyof typeof item)[] = [
'formId',
'nativeId',
'email',
'name',
'role',
'signingOrder',
'actionAuth',
];
keys.forEach((key) => {
form.setValue(`signers.${index}.${key}` as const, item[key]);
});
});
const currentLength = form.getValues('signers').length; const lastSigner = updatedSigners[updatedSigners.length - 1];
if (currentLength > updatedSigners.length) { if (lastSigner.role === RecipientRole.ASSISTANT) {
for (let i = updatedSigners.length; i < currentLength; i++) { toast({
form.unregister(`signers.${i}`); title: _(msg`Warning: Assistant as last signer`),
} description: _(
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
),
});
} }
await form.trigger('signers'); await form.trigger('signers');
}, },
[form, canRecipientBeModified, watchedSigners], [form, canRecipientBeModified, watchedSigners, toast],
); );
const triggerDragAndDrop = useCallback( const handleRoleChange = useCallback(
(fromIndex: number, toIndex: number) => { (index: number, role: RecipientRole) => {
if (!$sensorApi.current) { const currentSigners = form.getValues('signers');
const signingOrder = form.getValues('signingOrder');
// Handle parallel to sequential conversion for assistants
if (role === RecipientRole.ASSISTANT && signingOrder === DocumentSigningOrder.PARALLEL) {
form.setValue('signingOrder', DocumentSigningOrder.SEQUENTIAL);
toast({
title: _(msg`Signing order is enabled.`),
description: _(msg`You cannot add assistants when signing order is disabled.`),
variant: 'destructive',
});
return; return;
} }
const draggableId = signers[fromIndex].id; const updatedSigners = currentSigners.map((signer, idx) => ({
...signer,
role: idx === index ? role : signer.role,
signingOrder: !canRecipientBeModified(signer.nativeId) ? signer.signingOrder : idx + 1,
}));
const preDrag = $sensorApi.current.tryGetLock(draggableId); form.setValue('signers', updatedSigners);
if (!preDrag) { if (role === RecipientRole.ASSISTANT && index === updatedSigners.length - 1) {
return; toast({
title: _(msg`Warning: Assistant as last signer`),
description: _(
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
),
});
} }
const drag = preDrag.snapLift();
setTimeout(() => {
// Move directly to the target index
if (fromIndex < toIndex) {
for (let i = fromIndex; i < toIndex; i++) {
drag.moveDown();
}
} else {
for (let i = fromIndex; i > toIndex; i--) {
drag.moveUp();
}
}
setTimeout(() => {
drag.drop();
}, 500);
}, 0);
}, },
[signers], [form, toast, canRecipientBeModified],
);
const updateSigningOrders = useCallback(
(newIndex: number, oldIndex: number) => {
const updatedSigners = form.getValues('signers').map((signer, index) => {
if (index === oldIndex) {
return { ...signer, signingOrder: newIndex + 1 };
} else if (index >= newIndex && index < oldIndex) {
return {
...signer,
signingOrder: !canRecipientBeModified(signer.nativeId)
? signer.signingOrder
: (signer.signingOrder ?? index + 1) + 1,
};
} else if (index <= newIndex && index > oldIndex) {
return {
...signer,
signingOrder: !canRecipientBeModified(signer.nativeId)
? signer.signingOrder
: Math.max(1, (signer.signingOrder ?? index + 1) - 1),
};
}
return signer;
});
updatedSigners.forEach((signer, index) => {
form.setValue(`signers.${index}.signingOrder`, signer.signingOrder);
});
},
[form, canRecipientBeModified],
); );
const handleSigningOrderChange = useCallback( const handleSigningOrderChange = useCallback(
(index: number, newOrderString: string) => { (index: number, newOrderString: string) => {
const newOrder = parseInt(newOrderString, 10); const trimmedOrderString = newOrderString.trim();
if (!trimmedOrderString) {
if (!newOrderString.trim()) {
return; return;
} }
if (Number.isNaN(newOrder)) { const newOrder = Number(trimmedOrderString);
form.setValue(`signers.${index}.signingOrder`, index + 1); if (!Number.isInteger(newOrder) || newOrder < 1) {
return; return;
} }
const newIndex = newOrder - 1; const currentSigners = form.getValues('signers');
if (index !== newIndex) { const signer = currentSigners[index];
updateSigningOrders(newIndex, index);
triggerDragAndDrop(index, newIndex); // Remove signer from current position and insert at new position
const remainingSigners = currentSigners.filter((_, idx) => idx !== index);
const newPosition = Math.min(Math.max(0, newOrder - 1), currentSigners.length - 1);
remainingSigners.splice(newPosition, 0, signer);
const updatedSigners = remainingSigners.map((s, idx) => ({
...s,
signingOrder: !canRecipientBeModified(s.nativeId) ? s.signingOrder : idx + 1,
}));
form.setValue('signers', updatedSigners);
if (signer.role === RecipientRole.ASSISTANT && newPosition === remainingSigners.length - 1) {
toast({
title: _(msg`Warning: Assistant as last signer`),
description: _(
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
),
});
} }
}, },
[form, triggerDragAndDrop, updateSigningOrders], [form, canRecipientBeModified, toast],
); );
const handleSigningOrderDisable = useCallback(() => {
setShowSigningOrderConfirmation(false);
const currentSigners = form.getValues('signers');
const updatedSigners = currentSigners.map((signer) => ({
...signer,
role: signer.role === RecipientRole.ASSISTANT ? RecipientRole.SIGNER : signer.role,
}));
form.setValue('signers', updatedSigners);
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL);
}, [form]);
return ( return (
<> <>
<DocumentFlowFormContainerHeader <DocumentFlowFormContainerHeader
@ -384,11 +384,16 @@ export const AddSignersFormPartial = ({
{...field} {...field}
id="signingOrder" id="signingOrder"
checked={field.value === DocumentSigningOrder.SEQUENTIAL} checked={field.value === DocumentSigningOrder.SEQUENTIAL}
onCheckedChange={(checked) => onCheckedChange={(checked) => {
if (!checked && hasAssistantRole) {
setShowSigningOrderConfirmation(true);
return;
}
field.onChange( field.onChange(
checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL, checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL,
) );
} }}
disabled={isSubmitting || hasDocumentBeenSent} disabled={isSubmitting || hasDocumentBeenSent}
/> />
</FormControl> </FormControl>
@ -402,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={[
@ -613,7 +647,11 @@ export const AddSignersFormPartial = ({
<FormControl> <FormControl>
<RecipientRoleSelect <RecipientRoleSelect
{...field} {...field}
onValueChange={field.onChange} isAssistantEnabled={isSigningOrderSequential}
onValueChange={(value) =>
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
handleRoleChange(index, value as RecipientRole)
}
disabled={ disabled={
snapshot.isDragging || snapshot.isDragging ||
isSubmitting || isSubmitting ||
@ -710,6 +748,12 @@ export const AddSignersFormPartial = ({
)} )}
</Form> </Form>
</AnimateGenericFadeInOut> </AnimateGenericFadeInOut>
<SigningOrderConfirmation
open={showSigningOrderConfirmation}
onOpenChange={setShowSigningOrderConfirmation}
onConfirm={handleSigningOrderDisable}
/>
</DocumentFlowFormContainerContent> </DocumentFlowFormContainerContent>
<DocumentFlowFormContainerFooter> <DocumentFlowFormContainerFooter>

View File

@ -25,6 +25,7 @@ export const ZAddSignersFormSchema = z
}), }),
), ),
signingOrder: z.nativeEnum(DocumentSigningOrder), signingOrder: z.nativeEnum(DocumentSigningOrder),
modifyNextSigner: z.boolean(),
}) })
.refine( .refine(
(schema) => { (schema) => {

View File

@ -59,10 +59,10 @@ export const FieldIcon = ({
if (fieldMeta && (type === 'TEXT' || type === 'NUMBER')) { if (fieldMeta && (type === 'TEXT' || type === 'NUMBER')) {
if (type === 'TEXT' && 'text' in fieldMeta && fieldMeta.text && !fieldMeta.label) { if (type === 'TEXT' && 'text' in fieldMeta && fieldMeta.text && !fieldMeta.label) {
label = label =
fieldMeta.text.length > 10 ? fieldMeta.text.substring(0, 10) + '...' : fieldMeta.text; fieldMeta.text.length > 20 ? fieldMeta.text.substring(0, 20) + '...' : fieldMeta.text;
} else if (fieldMeta.label) { } else if (fieldMeta.label) {
label = label =
fieldMeta.label.length > 10 ? fieldMeta.label.substring(0, 10) + '...' : fieldMeta.label; fieldMeta.label.length > 20 ? fieldMeta.label.substring(0, 20) + '...' : fieldMeta.label;
} else { } else {
label = fieldIcons[type]?.label; label = fieldIcons[type]?.label;
} }

View File

@ -0,0 +1,40 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@documenso/ui/primitives/alert-dialog';
export type SigningOrderConfirmationProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: () => void;
};
export function SigningOrderConfirmation({
open,
onOpenChange,
onConfirm,
}: SigningOrderConfirmationProps) {
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Warning</AlertDialogTitle>
<AlertDialogDescription>
You have an assistant role on the signers list, removing the signing order will change
the assistant role to signer.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onConfirm}>Proceed</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View File

@ -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,
}; };

View File

@ -19,18 +19,18 @@ RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroupItem = React.forwardRef< const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>, React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, children: _children, ...props }, ref) => { >(({ className, ...props }, ref) => {
return ( return (
<RadioGroupPrimitive.Item <RadioGroupPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
'border-input ring-offset-background focus:ring-ring h-4 w-4 rounded-full border focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', 'border-primary text-primary focus-visible:ring-ring aspect-square h-4 w-4 rounded-full border shadow focus:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
className, className,
)} )}
{...props} {...props}
> >
<RadioGroupPrimitive.Indicator className="flex items-center justify-center"> <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="fill-primary text-primary h-2.5 w-2.5" /> <Circle className="fill-primary h-2.5 w-2.5" />
</RadioGroupPrimitive.Indicator> </RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item> </RadioGroupPrimitive.Item>
); );

View File

@ -1,4 +1,4 @@
import { BadgeCheck, Copy, Eye, PencilLine } from 'lucide-react'; import { BadgeCheck, Copy, Eye, PencilLine, User } from 'lucide-react';
import type { RecipientRole } from '.prisma/client'; import type { RecipientRole } from '.prisma/client';
@ -7,4 +7,5 @@ export const ROLE_ICONS: Record<RecipientRole, JSX.Element> = {
APPROVER: <BadgeCheck className="h-4 w-4" />, APPROVER: <BadgeCheck className="h-4 w-4" />,
CC: <Copy className="h-4 w-4" />, CC: <Copy className="h-4 w-4" />,
VIEWER: <Eye className="h-4 w-4" />, VIEWER: <Eye className="h-4 w-4" />,
ASSISTANT: <User className="h-4 w-4" />,
}; };

View File

@ -32,7 +32,7 @@ import {
import { nanoid } from '@documenso/lib/universal/id'; import { nanoid } from '@documenso/lib/universal/id';
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n'; import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
import type { Field, Recipient } from '@documenso/prisma/client'; import type { Field, Recipient } from '@documenso/prisma/client';
import { FieldType, RecipientRole } from '@documenso/prisma/client'; import { FieldType, RecipientRole, SendStatus } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -438,6 +438,7 @@ export const AddTemplateFieldsFormPartial = ({
VIEWER: [], VIEWER: [],
SIGNER: [], SIGNER: [],
APPROVER: [], APPROVER: [],
ASSISTANT: [],
}; };
recipients.forEach((recipient) => { recipients.forEach((recipient) => {
@ -447,10 +448,25 @@ export const AddTemplateFieldsFormPartial = ({
return recipientsByRole; return recipientsByRole;
}, [recipients]); }, [recipients]);
useEffect(() => {
const recipientsByRoleToDisplay = recipients.filter(
(recipient) =>
recipient.role !== RecipientRole.CC && recipient.role !== RecipientRole.ASSISTANT,
);
setSelectedSigner(
recipientsByRoleToDisplay.find((r) => r.sendStatus !== SendStatus.SENT) ??
recipientsByRoleToDisplay[0],
);
}, [recipients]);
const recipientsByRoleToDisplay = useMemo(() => { const recipientsByRoleToDisplay = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][]).filter( return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][]).filter(
([role]) => role !== RecipientRole.CC && role !== RecipientRole.VIEWER, ([role]) =>
role !== RecipientRole.CC &&
role !== RecipientRole.VIEWER &&
role !== RecipientRole.ASSISTANT,
); );
}, [recipientsByRole]); }, [recipientsByRole]);

View File

@ -29,6 +29,7 @@ import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message'; import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { toast } from '@documenso/ui/primitives/use-toast';
import { Checkbox } from '../checkbox'; import { Checkbox } from '../checkbox';
import { import {
@ -39,6 +40,7 @@ import {
DocumentFlowFormContainerStep, DocumentFlowFormContainerStep,
} from '../document-flow/document-flow-root'; } from '../document-flow/document-flow-root';
import { ShowFieldItem } from '../document-flow/show-field-item'; import { ShowFieldItem } from '../document-flow/show-field-item';
import { SigningOrderConfirmation } from '../document-flow/signing-order-confirmation';
import type { DocumentFlowStep } from '../document-flow/types'; import type { DocumentFlowStep } from '../document-flow/types';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
import { useStep } from '../stepper'; import { useStep } from '../stepper';
@ -213,41 +215,30 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
const items = Array.from(watchedSigners); const items = Array.from(watchedSigners);
const [reorderedSigner] = items.splice(result.source.index, 1); const [reorderedSigner] = items.splice(result.source.index, 1);
const insertIndex = result.destination.index; const insertIndex = result.destination.index;
items.splice(insertIndex, 0, reorderedSigner); items.splice(insertIndex, 0, reorderedSigner);
const updatedSigners = items.map((item, index) => ({ const updatedSigners = items.map((signer, index) => ({
...item, ...signer,
signingOrder: index + 1, signingOrder: index + 1,
})); }));
updatedSigners.forEach((item, index) => { form.setValue('signers', updatedSigners);
const keys: (keyof typeof item)[] = [
'formId',
'nativeId',
'email',
'name',
'role',
'signingOrder',
'actionAuth',
];
keys.forEach((key) => {
form.setValue(`signers.${index}.${key}` as const, item[key]);
});
});
const currentLength = form.getValues('signers').length; const lastSigner = updatedSigners[updatedSigners.length - 1];
if (currentLength > updatedSigners.length) { if (lastSigner.role === RecipientRole.ASSISTANT) {
for (let i = updatedSigners.length; i < currentLength; i++) { toast({
form.unregister(`signers.${i}`); title: _(msg`Warning: Assistant as last signer`),
} description: _(
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
),
});
} }
await form.trigger('signers'); await form.trigger('signers');
}, },
[form, watchedSigners], [form, watchedSigners, toast],
); );
const triggerDragAndDrop = useCallback( const triggerDragAndDrop = useCallback(
@ -308,26 +299,94 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
const handleSigningOrderChange = useCallback( const handleSigningOrderChange = useCallback(
(index: number, newOrderString: string) => { (index: number, newOrderString: string) => {
const newOrder = parseInt(newOrderString, 10); const trimmedOrderString = newOrderString.trim();
if (!trimmedOrderString) {
if (!newOrderString.trim()) {
return; return;
} }
if (Number.isNaN(newOrder)) { const newOrder = Number(trimmedOrderString);
form.setValue(`signers.${index}.signingOrder`, index + 1); if (!Number.isInteger(newOrder) || newOrder < 1) {
return; return;
} }
const newIndex = newOrder - 1; const currentSigners = form.getValues('signers');
if (index !== newIndex) { const signer = currentSigners[index];
updateSigningOrders(newIndex, index);
triggerDragAndDrop(index, newIndex); // Remove signer from current position and insert at new position
const remainingSigners = currentSigners.filter((_, idx) => idx !== index);
const newPosition = Math.min(Math.max(0, newOrder - 1), currentSigners.length - 1);
remainingSigners.splice(newPosition, 0, signer);
const updatedSigners = remainingSigners.map((s, idx) => ({
...s,
signingOrder: idx + 1,
}));
form.setValue('signers', updatedSigners);
if (signer.role === RecipientRole.ASSISTANT && newPosition === remainingSigners.length - 1) {
toast({
title: _(msg`Warning: Assistant as last signer`),
description: _(
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
),
});
} }
}, },
[form, triggerDragAndDrop, updateSigningOrders], [form, toast],
); );
const handleRoleChange = useCallback(
(index: number, role: RecipientRole) => {
const currentSigners = form.getValues('signers');
const signingOrder = form.getValues('signingOrder');
// Handle parallel to sequential conversion for assistants
if (role === RecipientRole.ASSISTANT && signingOrder === DocumentSigningOrder.PARALLEL) {
form.setValue('signingOrder', DocumentSigningOrder.SEQUENTIAL);
toast({
title: _(msg`Signing order is enabled.`),
description: _(msg`You cannot add assistants when signing order is disabled.`),
variant: 'destructive',
});
return;
}
const updatedSigners = currentSigners.map((signer, idx) => ({
...signer,
role: idx === index ? role : signer.role,
signingOrder: idx + 1,
}));
form.setValue('signers', updatedSigners);
if (role === RecipientRole.ASSISTANT && index === updatedSigners.length - 1) {
toast({
title: _(msg`Warning: Assistant as last signer`),
description: _(
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
),
});
}
},
[form, toast],
);
const [showSigningOrderConfirmation, setShowSigningOrderConfirmation] = useState(false);
const handleSigningOrderDisable = useCallback(() => {
setShowSigningOrderConfirmation(false);
const currentSigners = form.getValues('signers');
const updatedSigners = currentSigners.map((signer) => ({
...signer,
role: signer.role === RecipientRole.ASSISTANT ? RecipientRole.SIGNER : signer.role,
}));
form.setValue('signers', updatedSigners);
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL);
}, [form]);
return ( return (
<> <>
<DocumentFlowFormContainerHeader <DocumentFlowFormContainerHeader
@ -353,11 +412,19 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
{...field} {...field}
id="signingOrder" id="signingOrder"
checked={field.value === DocumentSigningOrder.SEQUENTIAL} checked={field.value === DocumentSigningOrder.SEQUENTIAL}
onCheckedChange={(checked) => onCheckedChange={(checked) => {
if (
!checked &&
watchedSigners.some((s) => s.role === RecipientRole.ASSISTANT)
) {
setShowSigningOrderConfirmation(true);
return;
}
field.onChange( field.onChange(
checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL, checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL,
) );
} }}
disabled={isSubmitting} disabled={isSubmitting}
/> />
</FormControl> </FormControl>
@ -556,7 +623,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
<FormControl> <FormControl>
<RecipientRoleSelect <RecipientRoleSelect
{...field} {...field}
onValueChange={field.onChange} onValueChange={(value) =>
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
handleRoleChange(index, value as RecipientRole)
}
disabled={isSubmitting} disabled={isSubmitting}
hideCCRecipients={isSignerDirectRecipient(signer)} hideCCRecipients={isSignerDirectRecipient(signer)}
/> />
@ -677,6 +747,12 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
onGoNextClick={() => void onFormSubmit()} onGoNextClick={() => void onFormSubmit()}
/> />
</DocumentFlowFormContainerFooter> </DocumentFlowFormContainerFooter>
<SigningOrderConfirmation
open={showSigningOrderConfirmation}
onOpenChange={setShowSigningOrderConfirmation}
onConfirm={handleSigningOrderDisable}
/>
</> </>
); );
}; };