Compare commits

..

43 Commits

Author SHA1 Message Date
b176ff884f fix: merge conflicts 2025-01-30 11:28:05 +00:00
667b8f1a85 chore: add access token to all seal-document 2025-01-30 11:26:05 +00:00
00429038ac fix: qr code link undefined 2025-01-30 11:17:18 +00:00
810d194cfd feat: return the necessary information for document preview and download 2025-01-30 10:29:22 +00:00
6743a108c0 chore: use correct link for qrcode 2025-01-30 10:17:59 +00:00
d82a759acd feat: add document view page 2025-01-30 10:12:25 +00:00
edfb1f2157 feat: add document access token model 2025-01-30 05:38:03 +00:00
7cc85ca6bc chore: extract translations 2025-01-30 16:03:36 +11:00
bc19fa0cbd feat: add Polish and Italian (#1618) 2025-01-30 15:21:37 +11:00
a60f58e20b chore: add translations (#1617) 2025-01-30 15:09:35 +11:00
aca902b5ff chore: add translations (#1585)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-01-30 13:24:46 +11:00
2f866c41b4 fix: create global settings on team creation (#1601)
The global team settings weren't created when creating a new team.

## Changes Made

The global team settings are now created when a new team is created.
2025-01-28 16:16:18 +11:00
7e4faef95f chore: add cancelled webhook event (#1608)
https://github.com/user-attachments/assets/9f2ff975-6688-4150-b4e3-0eb21e2b5503
2025-01-28 15:34:22 +11:00
bcef84787d feat: bulk send templates via csv (#1578)
Implements a bulk send feature allowing users to upload a CSV file to
create multiple documents from a template. Includes CSV template
generation, background processing, and email notifications.

<img width="563" alt="image"
src="https://github.com/user-attachments/assets/658cee71-6508-4a00-87da-b17c6762b7d8"
/>
<img width="578" alt="image"
src="https://github.com/user-attachments/assets/bbfac70b-c6a0-466a-be98-99ca4c4eb1b9"
/>
<img width="635" alt="image"
src="https://github.com/user-attachments/assets/65b2f55d-d491-45ac-84d6-1a31afe953dd"
/>


## Changes Made

- Added `TemplateBulkSendDialog` with CSV upload/download functionality
- Implemented bulk send job handler using background task system
- Created email template for completion notifications
- Added bulk send option to template view and actions dropdown
- Added CSV parsing with email/name validation

## Testing Performed

- CSV upload with valid/invalid data
- Bulk send with/without immediate sending
- Email notifications and error handling
- Team context integration
- File size and row count limits

Resolves #1550
2025-01-28 15:33:32 +11:00
70a3ac0525 fix: tidy document invite email render logic (#1597)
Updates one of our confusing ternaries to use `ts-pattern` for rendering
the conditional blocks making it easy to follow the logic occurring.

## Related Issue

N/A

## Changes Made

- Swapped ternary for `ts-pattern`

## Testing Performed

- Manually created a bunch of documents in configurations matching those
required to exhaust the `match` conditions.
2025-01-28 15:18:12 +11:00
c6fb101a99 fix: admin leaderboard query sorting (#1548) 2025-01-28 13:05:40 +11:00
2984af769c feat: add text align option to fields (#1610)
## Description

Adds the ability to align text to the left, center or right for relevant
fields.

Previously text was always centered which can be less desirable.

See attached debug document which has left, center and right text
alignments set for fields.

<img width="614" alt="image"
src="https://github.com/user-attachments/assets/361a030e-813d-458b-9c7a-ff4c9fa5e33c"
/>


## Related Issue

N/A

## Changes Made

- Added text align option
- Update the insert in pdf method to support different alignments
- Added a debug mode to field insertion

## Testing Performed

- Ran manual tests using the debug mode
2025-01-28 12:29:38 +11:00
9183f668d3 chore: bump node version for docker 2025-01-27 12:20:04 +11:00
54ea96391a fix: correct redirect after document duplication (#1595) 2025-01-23 16:34:22 +11:00
42d24fd1a1 feat: copy, paste, duplicate template fields (#1594) 2025-01-23 14:28:26 +11:00
dc36a8182c v1.9.0-rc.11 2025-01-21 09:49:22 +11:00
0ef85b47b1 fix: handle empty object as fieldMeta 2025-01-21 09:46:54 +11:00
058d9dd0ba v1.9.0-rc.10 2025-01-20 19:54:39 +11:00
74bb230247 fix: add empty success responses (#1600) 2025-01-20 19:47:39 +11:00
7c1e0f34e8 v1.9.0-rc.9 2025-01-20 16:08:15 +11:00
7e31323faa fix: add team context to more vanilla client usages 2025-01-20 15:53:28 +11:00
a28cdf437b feat: add get field endpoints (#1599) 2025-01-20 15:53:12 +11:00
80dfbeb16f feat: add angular embedding docs (#1592) 2025-01-20 09:35:47 +11:00
9de3a32ceb fix: pass team id to vanilla trpc client 2025-01-20 09:30:36 +11:00
0d3864548c fix: bump trpc and openapi packages (#1591) 2025-01-19 22:07:02 +11:00
9e03747e43 feat: add create document beta endpoint (#1584) 2025-01-16 13:36:00 +11:00
5750f2b477 feat: add prisma json types (#1583) 2025-01-15 13:46:45 +11:00
901be70f97 feat: add consistent response schemas (#1582) 2025-01-14 00:43:35 +11:00
7d0a9c6439 fix: refactor prisma relations (#1581) 2025-01-13 13:41:53 +11:00
48b55758e3 feat: ignore unrecognized fields from authorization response (#1479)
When authenticating using OIDC some IDPs send additional fields in their
authorization response. 

This leads to an error because these fields can't be persisted to the DB 
through the auth.js prisma adapter. 

This PR solves this by deleting all unrecognized fields from the 
authorization response before persisting. 

This behaviour is also compliant to 
[RFC6749 Section 4.1.2](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2)
2025-01-13 11:05:37 +11:00
dcaccb65f2 v1.9.0-rc.8 2025-01-13 10:21:05 +11:00
723e1b4ea2 fix: include all template meta in findTemplates 2025-01-13 09:34:23 +11:00
04e5244a5a chore: update styling 2025-01-09 14:38:00 +11:00
e367894218 Merge branch 'main' into feat/document-qrcode 2025-01-09 13:15:15 +11:00
99b0b80635 chore: use uqr 2025-01-07 13:05:52 +00:00
1b1cd0ba3d Merge branch 'main' into feat/document-qrcode 2025-01-07 12:59:21 +00:00
1fe5cda0ed Merge branch 'main' into feat/document-qrcode 2025-01-03 03:07:05 +00:00
f82e71f11c feat: qr code 2025-01-03 03:05:01 +00:00
307 changed files with 24129 additions and 9203 deletions

View File

@ -10,7 +10,7 @@ on:
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
permissions:
actions: read
contents: read

View File

@ -8,7 +8,7 @@ jobs:
e2e_tests:
name: 'E2E Tests'
timeout-minutes: 60
runs-on: ubuntu-22.04
runs-on: warp-ubuntu-2204-x64-16x
steps:
- uses: actions/checkout@v4

View File

@ -16,7 +16,7 @@
"@documenso/tailwind-config": "*",
"@documenso/trpc": "*",
"@documenso/ui": "*",
"next": "14.2.23",
"next": "14.2.6",
"next-plausible": "^3.12.0",
"nextra": "^2.13.4",
"nextra-theme-docs": "^2.13.4",
@ -27,6 +27,6 @@
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"typescript": "5.2.2"
"typescript": "5.6.2"
}
}

View File

@ -5,5 +5,6 @@
"svelte": "Svelte Integration",
"solid": "Solid Integration",
"preact": "Preact Integration",
"angular": "Angular Integration",
"css-variables": "CSS Variables"
}
}

View File

@ -0,0 +1,90 @@
---
title: Angular Integration
description: Learn how to use our embedding SDK within your Angular application.
---
# Angular Integration
Our Angular SDK provides a simple way to embed a signing experience within your Angular application. It supports both direct link templates and signing tokens.
## Installation
To install the SDK, run the following command:
```bash
npm install @documenso/embed-angular
```
## Usage
To embed a signing experience, you'll need to provide the token for the document you want to embed. This can be done in a few different ways, depending on your use case.
### Direct Link Template
If you have a direct link template, you can simply provide the token for the template to the `EmbedDirectTemplate` component.
```typescript
import { Component } from '@angular/core';
import { EmbedDirectTemplate } from '@documenso/embed-angular';
@Component({
selector: 'app-embedding',
template: `
<embed-direct-template [token]="token" />
`,
standalone: true,
imports: [EmbedDirectTemplate],
})
export class EmbeddingComponent {
token = 'YOUR_TOKEN_HERE'; // Replace with the actual token
}
```
#### Props
| Prop | Type | Description |
| ------------------- | ------------------- | ------------------------------------------------------------------------------------------ |
| token | string | The token for the document you want to embed |
| host | string (optional) | The host to be used for the signing experience, relevant for self-hosters |
| name | string (optional) | The name the signer that will be used by default for signing |
| lockName | boolean (optional) | Whether or not the name field should be locked disallowing modifications |
| email | string (optional) | The email the signer that will be used by default for signing |
| lockEmail | boolean (optional) | Whether or not the email field should be locked disallowing modifications |
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |
| onFieldSigned | function (optional) | A callback function that will be called when a field is signed |
| onFieldUnsigned | function (optional) | A callback function that will be called when a field is unsigned |
### Signing Token
If you have a signing token, you can provide it to the `EmbedSignDocument` component.
```typescript
import { Component } from '@angular/core';
import { EmbedSignDocument } from '@documenso/embed-angular';
@Component({
selector: 'app-embedding',
template: `
<embed-sign-document [token]="token" />
`,
standalone: true,
imports: [EmbedSignDocument],
})
export class EmbeddingComponent {
token = 'YOUR_TOKEN_HERE'; // Replace with the actual token
}
```
#### Props
| Prop | Type | Description |
| ------------------- | ------------------- | ------------------------------------------------------------------------------------------ |
| token | string | The token for the document you want to embed |
| host | string (optional) | The host to be used for the signing experience, relevant for self-hosters |
| name | string (optional) | The name the signer that will be used by default for signing |
| lockName | boolean (optional) | Whether or not the name field should be locked disallowing modifications |
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |

View File

@ -5,7 +5,7 @@ description: Learn how to use embedding to bring signing to your own website or
# Embedding
Our embedding feature lets you integrate our document signing experience into your own application or website. Whether you're building with React, Preact, Vue, Svelte, Solid, or using generalized web components, this guide will help you get started with embedding Documenso.
Our embedding feature lets you integrate our document signing experience into your own application or website. Whether you're building with React, Preact, Vue, Svelte, Solid, Angular, or using generalized web components, this guide will help you get started with embedding Documenso.
## Availability
@ -73,13 +73,14 @@ These customization options are available for both Direct Templates and Signing
We support embedding across a range of popular JavaScript frameworks, including:
| Framework | Package |
| --------- | -------------------------------------------------------------------------------- |
| React | [@documenso/embed-react](https://www.npmjs.com/package/@documenso/embed-react) |
| Preact | [@documenso/embed-preact](https://www.npmjs.com/package/@documenso/embed-preact) |
| Vue | [@documenso/embed-vue](https://www.npmjs.com/package/@documenso/embed-vue) |
| Svelte | [@documenso/embed-svelte](https://www.npmjs.com/package/@documenso/embed-svelte) |
| Solid | [@documenso/embed-solid](https://www.npmjs.com/package/@documenso/embed-solid) |
| Framework | Package |
| --------- | ---------------------------------------------------------------------------------- |
| React | [@documenso/embed-react](https://www.npmjs.com/package/@documenso/embed-react) |
| Preact | [@documenso/embed-preact](https://www.npmjs.com/package/@documenso/embed-preact) |
| Vue | [@documenso/embed-vue](https://www.npmjs.com/package/@documenso/embed-vue) |
| Svelte | [@documenso/embed-svelte](https://www.npmjs.com/package/@documenso/embed-svelte) |
| Solid | [@documenso/embed-solid](https://www.npmjs.com/package/@documenso/embed-solid) |
| Angular | [@documenso/embed-angular](https://www.npmjs.com/package/@documenso/embed-angular) |
Additionally, we provide **web components** for more generalized use. However, please note that web components are still in their early stages and haven't been extensively tested.
@ -127,7 +128,7 @@ This will show a dialog which will ask you to configure which recipient should b
## Embedding with Signing Tokens
To embed the signing process for an ordinary document, youll need a **document signing token** for the recipient. This token provides the necessary access to load the document and facilitate the signing process securely.
To embed the signing process for an ordinary document, you'll need a **document signing token** for the recipient. This token provides the necessary access to load the document and facilitate the signing process securely.
#### Instructions
@ -164,6 +165,7 @@ Once you've obtained the appropriate tokens, you can integrate the signing exper
- [Vue](/developers/embedding/vue)
- [Svelte](/developers/embedding/svelte)
- [Solid](/developers/embedding/solid)
- [Angular](/developers/embedding/angular)
If you're using **web components**, the integration process is slightly different. Keep in mind that web components are currently less tested but can still provide flexibility for general use cases.
@ -174,4 +176,5 @@ If you're using **web components**, the integration process is slightly differen
- [Svelte Integration](/developers/embedding/svelte)
- [Solid Integration](/developers/embedding/solid)
- [Preact Integration](/developers/embedding/preact)
- [Angular Integration](/developers/embedding/angular)
- [CSS Variables](/developers/embedding/css-variables)

View File

@ -21,6 +21,7 @@ Documenso supports Webhooks and allows you to subscribe to the following events:
- `document.signed`
- `document.completed`
- `document.rejected`
- `document.cancelled`
## Create a webhook subscription
@ -37,7 +38,7 @@ Clicking on the "**Create Webhook**" button opens a modal to create a new webhoo
To create a new webhook subscription, you need to provide the following information:
- Enter the webhook URL that will receive the event payload.
- Select the event(s) you want to subscribe to: `document.created`, `document.sent`, `document.opened`, `document.signed`, `document.completed`, `document.rejected`.
- Select the event(s) you want to subscribe to: `document.created`, `document.sent`, `document.opened`, `document.signed`, `document.completed`, `document.rejected`, `document.cancelled`.
- Optionally, you can provide a secret key that will be used to sign the payload. This key will be included in the `X-Documenso-Secret` header of the request.
![A screenshot of the Create Webhook modal that shows the URL input field and the event checkboxes](/webhook-images/webhooks-page-create-webhook-modal.webp)
@ -528,6 +529,96 @@ Example payload for the `document.rejected` event:
}
```
Example payload for the `document.rejected` event:
```json
{
"event": "DOCUMENT_CANCELLED",
"payload": {
"id": 7,
"externalId": null,
"userId": 3,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "documenso.pdf",
"status": "PENDING",
"documentDataId": "cm6exvn93006hi02ru90a265a",
"createdAt": "2025-01-27T11:02:14.393Z",
"updatedAt": "2025-01-27T11:03:16.387Z",
"completedAt": null,
"deletedAt": null,
"teamId": null,
"templateId": null,
"source": "DOCUMENT",
"documentMeta": {
"id": "cm6exvn96006ji02rqvzjvwoy",
"subject": "",
"message": "",
"timezone": "Etc/UTC",
"password": null,
"dateFormat": "yyyy-MM-dd hh:mm a",
"redirectUrl": "",
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": {
"documentDeleted": true,
"documentPending": true,
"recipientSigned": true,
"recipientRemoved": true,
"documentCompleted": true,
"ownerDocumentCompleted": true,
"recipientSigningRequest": true
}
},
"recipients": [
{
"id": 7,
"documentId": 7,
"templateId": null,
"email": "mybirihix@mailinator.com",
"name": "Zorita Baird",
"token": "XkKx1HCs6Znm2UBJA2j6o",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": { "accessAuth": null, "actionAuth": null },
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "NOT_OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "SENT"
}
],
"Recipient": [
{
"id": 7,
"documentId": 7,
"templateId": null,
"email": "signer@documenso.com",
"name": "Signer",
"token": "XkKx1HCs6Znm2UBJA2j6o",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": { "accessAuth": null, "actionAuth": null },
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "NOT_OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "SENT"
}
]
},
"createdAt": "2025-01-27T11:03:27.730Z",
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
}
```
## Availability
Webhooks are available to individual users and teams.

View File

@ -13,11 +13,11 @@
"dependencies": {
"@documenso/prisma": "*",
"luxon": "^3.5.0",
"next": "14.2.23"
"next": "14.2.6"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"typescript": "5.2.2"
"@types/react": "18.3.5",
"typescript": "5.6.2"
}
}

View File

@ -3,4 +3,4 @@
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@ -1,6 +1,6 @@
{
"name": "@documenso/web",
"version": "1.9.0-rc.7",
"version": "1.9.0-rc.11",
"private": true,
"license": "AGPL-3.0",
"scripts": {
@ -26,19 +26,18 @@
"@lingui/react": "^4.11.3",
"@simplewebauthn/browser": "^9.0.1",
"@simplewebauthn/server": "^9.0.3",
"@tanstack/react-query": "^4.29.5",
"colord": "^2.9.3",
"cookie-es": "^1.0.0",
"formidable": "^2.1.1",
"framer-motion": "^10.12.8",
"input-otp": "^1.2.4",
"lucide-react": "^0.279.0",
"luxon": "^3.5.0",
"luxon": "^3.4.0",
"micro": "^10.0.1",
"next": "14.2.23",
"next": "14.2.6",
"next-auth": "4.24.5",
"next-axiom": "^1.5.1",
"next-plausible": "^3.12.0",
"next-plausible": "^3.10.1",
"next-themes": "^0.2.1",
"papaparse": "^5.4.1",
"perfect-freehand": "^1.2.0",
@ -55,7 +54,7 @@
"recharts": "^2.7.2",
"remeda": "^2.17.3",
"sharp": "0.32.6",
"trpc-openapi": "^1.2.0",
"trpc-to-openapi": "2.0.4",
"ts-pattern": "^5.0.5",
"ua-parser-js": "^1.0.37",
"uqr": "^0.1.2",
@ -73,6 +72,6 @@
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/ua-parser-js": "^0.7.39",
"typescript": "5.2.2"
"typescript": "5.6.2"
}
}
}

View File

@ -0,0 +1,19 @@
<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>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -28,7 +28,7 @@ export const AdminActions = ({ className, document, recipients }: AdminActionsPr
const { _ } = useLingui();
const { toast } = useToast();
const { mutate: resealDocument, isLoading: isResealDocumentLoading } =
const { mutate: resealDocument, isPending: isResealDocumentLoading } =
trpc.admin.resealDocument.useMutation({
onSuccess: () => {
toast({

View File

@ -59,7 +59,7 @@ export default async function AdminDocumentDetailsPage({ params }: AdminDocument
<Trans>Admin Actions</Trans>
</h2>
<AdminActions className="mt-2" document={document} recipients={document.Recipient} />
<AdminActions className="mt-2" document={document} recipients={document.recipients} />
<hr className="my-4" />
<h2 className="text-lg font-semibold">
@ -68,7 +68,7 @@ export default async function AdminDocumentDetailsPage({ params }: AdminDocument
<div className="mt-4">
<Accordion type="multiple" className="space-y-4">
{document.Recipient.map((recipient) => (
{document.recipients.map((recipient) => (
<AccordionItem
key={recipient.id}
value={recipient.id.toString()}

View File

@ -39,9 +39,9 @@ type TAdminUpdateRecipientFormSchema = z.infer<typeof ZAdminUpdateRecipientFormS
export type RecipientItemProps = {
recipient: Recipient & {
Field: Array<
fields: Array<
Field & {
Signature: Signature | null;
signature: Signature | null;
}
>;
};
@ -89,13 +89,13 @@ export const RecipientItem = ({ recipient }: RecipientItemProps) => {
accessorKey: 'signature',
cell: ({ row }) => (
<div>
{row.original.Signature?.typedSignature && (
<span>{row.original.Signature.typedSignature}</span>
{row.original.signature?.typedSignature && (
<span>{row.original.signature.typedSignature}</span>
)}
{row.original.Signature?.signatureImageAsBase64 && (
{row.original.signature?.signatureImageAsBase64 && (
<img
src={row.original.Signature.signatureImageAsBase64}
src={row.original.signature.signatureImageAsBase64}
alt="Signature"
className="h-12 w-full dark:invert"
/>
@ -103,7 +103,7 @@ export const RecipientItem = ({ recipient }: RecipientItemProps) => {
</div>
),
},
] satisfies DataTableColumnDef<(typeof recipient)['Field'][number]>[];
] satisfies DataTableColumnDef<(typeof recipient)['fields'][number]>[];
}, []);
const onUpdateRecipientFormSubmit = async ({ name, email }: TAdminUpdateRecipientFormSchema) => {
@ -190,7 +190,7 @@ export const RecipientItem = ({ recipient }: RecipientItemProps) => {
<Trans>Fields</Trans>
</h2>
<DataTable columns={columns} data={recipient.Field} />
<DataTable columns={columns} data={recipient.fields} />
</div>
);
};

View File

@ -8,7 +8,6 @@ import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import type { Document } from '@documenso/prisma/client';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
@ -36,7 +35,7 @@ export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialo
const [reason, setReason] = useState('');
const { mutateAsync: deleteDocument, isLoading: isDeletingDocument } =
const { mutateAsync: deleteDocument, isPending: isDeletingDocument } =
trpc.admin.deleteDocument.useMutation();
const handleDeleteDocument = async () => {
@ -55,21 +54,12 @@ export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialo
router.push('/admin/documents');
} catch (err) {
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
toast({
title: _(msg`An error occurred`),
description: err.message,
variant: 'destructive',
});
} else {
toast({
title: _(msg`An unknown error occurred`),
variant: 'destructive',
description:
err.message ??
'We encountered an unknown error while attempting to delete your document. Please try again later.',
});
}
toast({
title: _(msg`An unknown error occurred`),
variant: 'destructive',
description:
'We encountered an unknown error while attempting to delete your document. Please try again later.',
});
}
};
@ -77,7 +67,7 @@ export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialo
<div>
<div>
<Alert
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row "
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<div>

View File

@ -37,7 +37,7 @@ export const AdminDocumentResults = () => {
const page = searchParams?.get?.('page') ? Number(searchParams.get('page')) : undefined;
const perPage = searchParams?.get?.('perPage') ? Number(searchParams.get('perPage')) : undefined;
const { data: findDocumentsData, isLoading: isFindDocumentsLoading } =
const { data: findDocumentsData, isPending: isFindDocumentsLoading } =
trpc.admin.findDocuments.useQuery(
{
query: debouncedTerm,
@ -45,7 +45,7 @@ export const AdminDocumentResults = () => {
perPage: perPage || 20,
},
{
keepPreviousData: true,
placeholderData: (previousData) => previousData,
},
);
@ -86,14 +86,14 @@ export const AdminDocumentResults = () => {
header: _(msg`Owner`),
accessorKey: 'owner',
cell: ({ row }) => {
const avatarFallbackText = row.original.User.name
? extractInitials(row.original.User.name)
: row.original.User.email.slice(0, 1).toUpperCase();
const avatarFallbackText = row.original.user.name
? extractInitials(row.original.user.name)
: row.original.user.email.slice(0, 1).toUpperCase();
return (
<Tooltip delayDuration={200}>
<TooltipTrigger>
<Link href={`/admin/users/${row.original.User.id}`}>
<Link href={`/admin/users/${row.original.user.id}`}>
<Avatar className="dark:border-border h-12 w-12 border-2 border-solid border-white">
<AvatarFallback className="text-xs text-gray-400">
{avatarFallbackText}
@ -110,8 +110,8 @@ export const AdminDocumentResults = () => {
</Avatar>
<div className="text-muted-foreground flex flex-col text-sm">
<span>{row.original.User.name}</span>
<span>{row.original.User.email}</span>
<span>{row.original.user.name}</span>
<span>{row.original.user.email}</span>
</div>
</TooltipContent>
</Tooltip>

View File

@ -4,7 +4,7 @@ import { useEffect, useMemo, useState, useTransition } from 'react';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { ChevronDownIcon as CaretSortIcon, Loader } from 'lucide-react';
import { ChevronDownIcon, ChevronUpIcon, ChevronsUpDown, Loader } from 'lucide-react';
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
@ -54,7 +54,15 @@ export const LeaderboardTable = ({
onClick={() => handleColumnSort('name')}
>
{_(msg`Name`)}
<CaretSortIcon className="ml-2 h-4 w-4" />
{sortBy === 'name' ? (
sortOrder === 'asc' ? (
<ChevronUpIcon className="ml-2 h-4 w-4" />
) : (
<ChevronDownIcon className="ml-2 h-4 w-4" />
)
) : (
<ChevronsUpDown className="ml-2 h-4 w-4" />
)}
</div>
),
accessorKey: 'name',
@ -80,7 +88,15 @@ export const LeaderboardTable = ({
onClick={() => handleColumnSort('signingVolume')}
>
{_(msg`Signing Volume`)}
<CaretSortIcon className="ml-2 h-4 w-4" />
{sortBy === 'signingVolume' ? (
sortOrder === 'asc' ? (
<ChevronUpIcon className="ml-2 h-4 w-4" />
) : (
<ChevronDownIcon className="ml-2 h-4 w-4" />
)
) : (
<ChevronsUpDown className="ml-2 h-4 w-4" />
)}
</div>
),
accessorKey: 'signingVolume',
@ -94,7 +110,15 @@ export const LeaderboardTable = ({
onClick={() => handleColumnSort('createdAt')}
>
{_(msg`Created`)}
<CaretSortIcon className="ml-2 h-4 w-4" />
{sortBy === 'createdAt' ? (
sortOrder === 'asc' ? (
<ChevronUpIcon className="ml-2 h-4 w-4" />
) : (
<ChevronDownIcon className="ml-2 h-4 w-4" />
)
) : (
<ChevronsUpDown className="ml-2 h-4 w-4" />
)}
</div>
);
},
@ -102,7 +126,7 @@ export const LeaderboardTable = ({
cell: ({ row }) => i18n.date(row.original.createdAt),
},
] satisfies DataTableColumnDef<SigningVolume>[];
}, [sortOrder]);
}, [sortOrder, sortBy]);
useEffect(() => {
startTransition(() => {
@ -133,6 +157,9 @@ export const LeaderboardTable = ({
const handleColumnSort = (column: 'name' | 'createdAt' | 'signingVolume') => {
startTransition(() => {
updateSearchParams({
search: debouncedSearchString,
page,
perPage,
sortBy: column,
sortOrder: sortBy === column && sortOrder === 'asc' ? 'desc' : 'asc',
});

View File

@ -13,7 +13,6 @@ import {
SITE_SETTINGS_BANNER_ID,
ZSiteSettingsBannerSchema,
} from '@documenso/lib/server-only/site-settings/schemas/banner';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc as trpcReact } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import { ColorPicker } from '@documenso/ui/primitives/color-picker';
@ -59,7 +58,7 @@ export function BannerForm({ banner }: BannerFormProps) {
const enabled = form.watch('enabled');
const { mutateAsync: updateSiteSetting, isLoading: isUpdateSiteSettingLoading } =
const { mutateAsync: updateSiteSetting, isPending: isUpdateSiteSettingLoading } =
trpcReact.admin.updateSiteSetting.useMutation();
const onBannerUpdate = async ({ id, enabled, data }: TBannerFormSchema) => {
@ -78,21 +77,13 @@ export function BannerForm({ banner }: BannerFormProps) {
router.refresh();
} catch (err) {
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
toast({
title: _(msg`An error occurred`),
description: err.message,
variant: 'destructive',
});
} else {
toast({
title: _(msg`An unknown error occurred`),
variant: 'destructive',
description: _(
msg`We encountered an unknown error while attempting to update the banner. Please try again later.`,
),
});
}
toast({
title: _(msg`An unknown error occurred`),
variant: 'destructive',
description: _(
msg`We encountered an unknown error while attempting to update the banner. Please try again later.`,
),
});
}
};

View File

@ -6,9 +6,10 @@ import { useRouter } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { match } from 'ts-pattern';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { User } from '@documenso/prisma/client';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
@ -37,7 +38,7 @@ export const DeleteUserDialog = ({ className, user }: DeleteUserDialogProps) =>
const [email, setEmail] = useState('');
const { mutateAsync: deleteUser, isLoading: isDeletingUser } =
const { mutateAsync: deleteUser, isPending: isDeletingUser } =
trpc.admin.deleteUser.useMutation();
const onDeleteAccount = async () => {
@ -54,23 +55,19 @@ export const DeleteUserDialog = ({ className, user }: DeleteUserDialogProps) =>
router.push('/admin/users');
} catch (err) {
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
toast({
title: _(msg`An error occurred`),
description: err.message,
variant: 'destructive',
});
} else {
toast({
title: _(msg`An unknown error occurred`),
variant: 'destructive',
description:
err.message ??
_(
msg`We encountered an unknown error while attempting to delete your account. Please try again later.`,
),
});
}
const error = AppError.parseError(err);
const errorMessage = match(error.code)
.with(AppErrorCode.NOT_FOUND, () => msg`User not found.`)
.with(AppErrorCode.UNAUTHORIZED, () => msg`You are not authorized to delete this user.`)
.otherwise(() => msg`An error occurred while deleting the user.`);
toast({
title: _(msg`Error`),
description: _(errorMessage),
variant: 'destructive',
duration: 7500,
});
}
};

View File

@ -34,7 +34,7 @@ export const DisableUserDialog = ({ className, userToDisable }: DisableUserDialo
const [email, setEmail] = useState('');
const { mutateAsync: disableUser, isLoading: isDisablingUser } =
const { mutateAsync: disableUser, isPending: isDisablingUser } =
trpc.admin.disableUser.useMutation();
const onDisableAccount = async () => {

View File

@ -34,7 +34,7 @@ export const EnableUserDialog = ({ className, userToEnable }: EnableUserDialogPr
const [email, setEmail] = useState('');
const { mutateAsync: enableUser, isLoading: isEnablingUser } =
const { mutateAsync: enableUser, isPending: isEnablingUser } =
trpc.admin.enableUser.useMutation();
const onEnableAccount = async () => {

View File

@ -22,8 +22,8 @@ type UserData = {
name: string | null;
email: string;
roles: Role[];
Subscription?: SubscriptionLite[] | null;
Document: DocumentLite[];
subscriptions?: SubscriptionLite[] | null;
documents: DocumentLite[];
};
type SubscriptionLite = Pick<
@ -81,7 +81,7 @@ export const UsersDataTable = ({
header: _(msg`Subscription`),
accessorKey: 'subscription',
cell: ({ row }) => {
const foundIndividualSubscription = (row.original.Subscription ?? []).find((sub) =>
const foundIndividualSubscription = (row.original.subscriptions ?? []).find((sub) =>
individualPriceIds.includes(sub.priceId),
);
@ -92,7 +92,7 @@ export const UsersDataTable = ({
header: _(msg`Documents`),
accessorKey: 'documents',
cell: ({ row }) => {
return <div>{row.original.Document.length}</div>;
return <div>{row.original.documents?.length}</div>;
},
},
{

View File

@ -18,8 +18,8 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
export type DocumentPageViewButtonProps = {
document: Document & {
User: Pick<User, 'id' | 'name' | 'email'>;
Recipient: Recipient[];
user: Pick<User, 'id' | 'name' | 'email'>;
recipients: Recipient[];
team: Pick<Team, 'id' | 'url'> | null;
};
team?: Pick<Team, 'id' | 'url'>;
@ -34,7 +34,7 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
return null;
}
const recipient = document.Recipient.find((recipient) => recipient.email === session.user.email);
const recipient = document.recipients.find((recipient) => recipient.email === session.user.email);
const isRecipient = !!recipient;
const isPending = document.status === DocumentStatus.PENDING;
@ -46,9 +46,16 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
const onDownloadClick = async () => {
try {
const documentWithData = await trpcClient.document.getDocumentById.query({
documentId: document.id,
});
const documentWithData = await trpcClient.document.getDocumentById.query(
{
documentId: document.id,
},
{
context: {
teamId: document.team?.id?.toString(),
},
},
);
const documentData = documentWithData?.documentData;

View File

@ -41,8 +41,8 @@ import { DuplicateDocumentDialog } from '../duplicate-document-dialog';
export type DocumentPageViewDropdownProps = {
document: Document & {
User: Pick<User, 'id' | 'name' | 'email'>;
Recipient: Recipient[];
user: Pick<User, 'id' | 'name' | 'email'>;
recipients: Recipient[];
team: Pick<Team, 'id' | 'url'> | null;
};
team?: Pick<Team, 'id' | 'url'> & { teamEmail: TeamEmail | null };
@ -60,9 +60,9 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
return null;
}
const recipient = document.Recipient.find((recipient) => recipient.email === session.user.email);
const recipient = document.recipients.find((recipient) => recipient.email === session.user.email);
const isOwner = document.User.id === session.user.id;
const isOwner = document.user.id === session.user.id;
const isDraft = document.status === DocumentStatus.DRAFT;
const isPending = document.status === DocumentStatus.PENDING;
const isDeleted = document.deletedAt !== null;
@ -74,9 +74,16 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
const onDownloadClick = async () => {
try {
const documentWithData = await trpcClient.document.getDocumentById.query({
documentId: document.id,
});
const documentWithData = await trpcClient.document.getDocumentById.query(
{
documentId: document.id,
},
{
context: {
teamId: team?.id?.toString(),
},
},
);
const documentData = documentWithData?.documentData;
@ -94,7 +101,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
}
};
const nonSignedRecipients = document.Recipient.filter((item) => item.signingStatus !== 'SIGNED');
const nonSignedRecipients = document.recipients.filter((item) => item.signingStatus !== 'SIGNED');
return (
<DropdownMenu>
@ -149,7 +156,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
{canManageDocument && (
<DocumentRecipientLinkCopyDialog
recipients={document.Recipient}
recipients={document.recipients}
trigger={
<DropdownMenuItem
disabled={!isPending || isDeleted}

View File

@ -12,8 +12,8 @@ import type { Document, Recipient, User } from '@documenso/prisma/client';
export type DocumentPageViewInformationProps = {
userId: number;
document: Document & {
User: Pick<User, 'id' | 'name' | 'email'>;
Recipient: Recipient[];
user: Pick<User, 'id' | 'name' | 'email'>;
recipients: Recipient[];
};
};
@ -29,7 +29,8 @@ export const DocumentPageViewInformation = ({
return [
{
description: msg`Uploaded by`,
value: userId === document.userId ? _(msg`You`) : document.User.name ?? document.User.email,
value:
userId === document.userId ? _(msg`You`) : (document.user.name ?? document.user.email),
},
{
description: msg`Created`,

View File

@ -28,7 +28,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
export type DocumentPageViewRecipientsProps = {
document: Document & {
Recipient: Recipient[];
recipients: Recipient[];
};
documentRootPath: string;
};
@ -40,7 +40,7 @@ export const DocumentPageViewRecipients = ({
const { _ } = useLingui();
const { toast } = useToast();
const recipients = document.Recipient;
const recipients = document.recipients;
return (
<section className="dark:bg-background border-border bg-widget flex flex-col rounded-xl border">

View File

@ -71,7 +71,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
const documentVisibility = document?.visibility;
const currentTeamMemberRole = team?.currentTeamMember?.role;
const isRecipient = document?.Recipient.find((recipient) => recipient.email === user.email);
const isRecipient = document?.recipients.find((recipient) => recipient.email === user.email);
let canAccessDocument = true;
if (team && !isRecipient && document?.userId !== user.id) {
@ -131,7 +131,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
const documentWithRecipients = {
...document,
Recipient: recipients,
recipients,
};
return (

View File

@ -12,7 +12,7 @@ import {
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
SKIP_QUERY_BATCH_META,
} from '@documenso/lib/constants/trpc';
import type { TGetDocumentWithDetailsByIdResponse } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import type { TDocument } from '@documenso/lib/types/document';
import { DocumentDistributionMethod, DocumentStatus } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
@ -35,7 +35,7 @@ import { useOptionalCurrentTeam } from '~/providers/team';
export type EditDocumentFormProps = {
className?: string;
initialDocument: TGetDocumentWithDetailsByIdResponse;
initialDocument: TDocument;
documentRootPath: string;
isDocumentEnterprise: boolean;
};
@ -71,7 +71,7 @@ export const EditDocumentForm = ({
},
);
const { Recipient: recipients, Field: fields } = document;
const { recipients, fields } = document;
const { mutateAsync: updateDocument } = trpc.document.setSettingsForDocument.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
@ -105,7 +105,7 @@ export const EditDocumentForm = ({
{
documentId: initialDocument.id,
},
(oldData) => ({ ...(oldData || initialDocument), Field: newFields }),
(oldData) => ({ ...(oldData || initialDocument), fields: newFields }),
);
},
});
@ -117,7 +117,7 @@ export const EditDocumentForm = ({
{
documentId: initialDocument.id,
},
(oldData) => ({ ...(oldData || initialDocument), Recipient: newRecipients }),
(oldData) => ({ ...(oldData || initialDocument), recipients: newRecipients }),
);
},
});

View File

@ -52,7 +52,7 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
const documentVisibility = document?.visibility;
const currentTeamMemberRole = team?.currentTeamMember?.role;
const isRecipient = document?.Recipient.find((recipient) => recipient.email === user.email);
const isRecipient = document?.recipients.find((recipient) => recipient.email === user.email);
let canAccessDocument = true;
if (!isRecipient && document?.userId !== user.id) {
@ -78,7 +78,7 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
redirect(`${documentRootPath}/${documentId}`);
}
const { documentMeta, Recipient: recipients } = document;
const { documentMeta, recipients } = document;
if (documentMeta?.password) {
const key = DOCUMENSO_ENCRYPTION_KEY;

View File

@ -37,17 +37,16 @@ export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
const { data, isLoading, isInitialLoading, isLoadingError } =
trpc.document.findDocumentAuditLogs.useQuery(
{
documentId,
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
},
{
keepPreviousData: true,
},
);
const { data, isLoading, isLoadingError } = trpc.document.findDocumentAuditLogs.useQuery(
{
documentId,
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
},
{
placeholderData: (previousData) => previousData,
},
);
const onPaginationChange = (page: number, perPage: number) => {
updateSearchParams({
@ -132,7 +131,7 @@ export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps
enable: isLoadingError,
}}
skeleton={{
enable: isLoading && isInitialLoading,
enable: isLoading,
rows: 3,
component: (
<>

View File

@ -77,9 +77,9 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
},
{
description: msg`Created by`,
value: document.User.name
? `${document.User.name} (${document.User.email})`
: document.User.email,
value: document.user.name
? `${document.user.name} (${document.user.email})`
: document.user.email,
},
{
description: msg`Date created`,

View File

@ -19,7 +19,7 @@ export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditL
const { toast } = useToast();
const { _ } = useLingui();
const { mutateAsync: downloadAuditLogs, isLoading } =
const { mutateAsync: downloadAuditLogs, isPending } =
trpc.document.downloadAuditLogs.useMutation();
const onDownloadAuditLogsClick = async () => {
@ -70,10 +70,10 @@ export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditL
return (
<Button
className={cn('w-full sm:w-auto', className)}
loading={isLoading}
loading={isPending}
onClick={() => void onDownloadAuditLogsClick()}
>
{!isLoading && <DownloadIcon className="mr-1.5 h-4 w-4" />}
{!isPending && <DownloadIcon className="mr-1.5 h-4 w-4" />}
<Trans>Download Audit Logs</Trans>
</Button>
);

View File

@ -26,7 +26,7 @@ export const DownloadCertificateButton = ({
const { toast } = useToast();
const { _ } = useLingui();
const { mutateAsync: downloadCertificate, isLoading } =
const { mutateAsync: downloadCertificate, isPending } =
trpc.document.downloadCertificate.useMutation();
const onDownloadCertificatesClick = async () => {
@ -77,12 +77,12 @@ export const DownloadCertificateButton = ({
return (
<Button
className={cn('w-full sm:w-auto', className)}
loading={isLoading}
loading={isPending}
variant="outline"
disabled={documentStatus !== DocumentStatus.COMPLETED}
onClick={() => void onDownloadCertificatesClick()}
>
{!isLoading && <DownloadIcon className="mr-1.5 h-4 w-4" />}
{!isPending && <DownloadIcon className="mr-1.5 h-4 w-4" />}
<Trans>Download Certificate</Trans>
</Button>
);

View File

@ -12,15 +12,14 @@ import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import { trpc as trpcClient } from '@documenso/trpc/client';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type DataTableActionButtonProps = {
row: Document & {
User: Pick<User, 'id' | 'name' | 'email'>;
Recipient: Recipient[];
user: Pick<User, 'id' | 'name' | 'email'>;
recipients: Recipient[];
team: Pick<Team, 'id' | 'url'> | null;
};
team?: Pick<Team, 'id' | 'url'>;
@ -35,9 +34,9 @@ export const DataTableActionButton = ({ row, team }: DataTableActionButtonProps)
return null;
}
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
const recipient = row.recipients.find((recipient) => recipient.email === session.user.email);
const isOwner = row.User.id === session.user.id;
const isOwner = row.user.id === session.user.id;
const isRecipient = !!recipient;
const isDraft = row.status === DocumentStatus.DRAFT;
const isPending = row.status === DocumentStatus.PENDING;
@ -50,17 +49,20 @@ export const DataTableActionButton = ({ row, team }: DataTableActionButtonProps)
const onDownloadClick = async () => {
try {
let document: DocumentWithData | null = null;
if (!recipient) {
document = await trpcClient.document.getDocumentById.query({
documentId: row.id,
});
} else {
document = await trpcClient.document.getDocumentByToken.query({
token: recipient.token,
});
}
const document = !recipient
? await trpcClient.document.getDocumentById.query(
{
documentId: row.id,
},
{
context: {
teamId: team?.id?.toString(),
},
},
)
: await trpcClient.document.getDocumentByToken.query({
token: recipient.token,
});
const documentData = document?.documentData;

View File

@ -23,9 +23,8 @@ import { useSession } from 'next-auth/react';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { DocumentStatus, RecipientRole } from '@documenso/prisma/client';
import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import { DocumentStatus, RecipientRole } from '@documenso/prisma/client';
import { trpc as trpcClient } from '@documenso/trpc/client';
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
import {
@ -46,8 +45,8 @@ import { MoveDocumentDialog } from './move-document-dialog';
export type DataTableActionDropdownProps = {
row: Document & {
User: Pick<User, 'id' | 'name' | 'email'>;
Recipient: Recipient[];
user: Pick<User, 'id' | 'name' | 'email'>;
recipients: Recipient[];
team: Pick<Team, 'id' | 'url'> | null;
};
team?: Pick<Team, 'id' | 'url'> & { teamEmail?: string };
@ -66,9 +65,9 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
return null;
}
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
const recipient = row.recipients.find((recipient) => recipient.email === session.user.email);
const isOwner = row.User.id === session.user.id;
const isOwner = row.user.id === session.user.id;
// const isRecipient = !!recipient;
const isDraft = row.status === DocumentStatus.DRAFT;
const isPending = row.status === DocumentStatus.PENDING;
@ -81,17 +80,13 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
const onDownloadClick = async () => {
try {
let document: DocumentWithData | null = null;
if (!recipient) {
document = await trpcClient.document.getDocumentById.query({
documentId: row.id,
});
} else {
document = await trpcClient.document.getDocumentByToken.query({
token: recipient.token,
});
}
const document = !recipient
? await trpcClient.document.getDocumentById.query({
documentId: row.id,
})
: await trpcClient.document.getDocumentByToken.query({
token: recipient.token,
});
const documentData = document?.documentData;
@ -109,7 +104,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
}
};
const nonSignedRecipients = row.Recipient.filter((item) => item.signingStatus !== 'SIGNED');
const nonSignedRecipients = row.recipients.filter((item) => item.signingStatus !== 'SIGNED');
return (
<DropdownMenu>
@ -194,7 +189,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
{canManageDocument && (
<DocumentRecipientLinkCopyDialog
recipients={row.Recipient}
recipients={row.recipients}
trigger={
<DropdownMenuItem disabled={!isPending} asChild onSelect={(e) => e.preventDefault()}>
<div>
@ -238,14 +233,12 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
onOpenChange={setMoveDialogOpen}
/>
{isDuplicateDialogOpen && (
<DuplicateDocumentDialog
id={row.id}
open={isDuplicateDialogOpen}
onOpenChange={setDuplicateDialogOpen}
team={team}
/>
)}
<DuplicateDocumentDialog
id={row.id}
open={isDuplicateDialogOpen}
onOpenChange={setDuplicateDialogOpen}
team={team}
/>
</DropdownMenu>
);
};

View File

@ -25,7 +25,7 @@ export const DataTableSenderFilter = ({ teamId }: DataTableSenderFilterProps) =>
const senderIds = parseToIntegerArray(searchParams?.get('senderIds') ?? '');
const { data, isInitialLoading } = trpc.team.getTeamMembers.useQuery({
const { data, isLoading } = trpc.team.getTeamMembers.useQuery({
teamId,
});
@ -61,7 +61,7 @@ export const DataTableSenderFilter = ({ teamId }: DataTableSenderFilterProps) =>
}
enableClearAllButton={true}
inputPlaceholder={msg`Search`}
loading={!isMounted || isInitialLoading}
loading={!isMounted || isLoading}
options={comboBoxOptions}
selectedValues={senderIds}
onChange={onChange}

View File

@ -10,9 +10,9 @@ import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
export type DataTableTitleProps = {
row: Document & {
User: Pick<User, 'id' | 'name' | 'email'>;
user: Pick<User, 'id' | 'name' | 'email'>;
team: Pick<Team, 'url'> | null;
Recipient: Recipient[];
recipients: Recipient[];
};
teamUrl?: string;
};
@ -24,9 +24,9 @@ export const DataTableTitle = ({ row, teamUrl }: DataTableTitleProps) => {
return null;
}
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
const recipient = row.recipients.find((recipient) => recipient.email === session.user.email);
const isOwner = row.User.id === session.user.id;
const isOwner = row.user.id === session.user.id;
const isRecipient = !!recipient;
const isCurrentTeamDocument = teamUrl && row.team?.url === teamUrl;

View File

@ -9,9 +9,9 @@ import { DateTime } from 'luxon';
import { useSession } from 'next-auth/react';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import type { TFindDocumentsResponse } from '@documenso/lib/server-only/document/find-documents';
import type { Team } from '@documenso/prisma/client';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import type { TFindDocumentsResponse } from '@documenso/trpc/server/document-router/schema';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
@ -57,14 +57,14 @@ export const DocumentsDataTable = ({
{
id: 'sender',
header: _(msg`Sender`),
cell: ({ row }) => row.original.User.name ?? row.original.User.email,
cell: ({ row }) => row.original.user.name ?? row.original.user.email,
},
{
header: _(msg`Recipient`),
accessorKey: 'recipient',
cell: ({ row }) => (
<StackAvatarsWithTooltip
recipients={row.original.Recipient}
recipients={row.original.recipients}
documentStatus={row.original.status}
/>
),

View File

@ -51,7 +51,7 @@ export const DeleteDocumentDialog = ({
const [inputValue, setInputValue] = useState('');
const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
const { mutateAsync: deleteDocument, isLoading } = trpcReact.document.deleteDocument.useMutation({
const { mutateAsync: deleteDocument, isPending } = trpcReact.document.deleteDocument.useMutation({
onSuccess: () => {
router.refresh();
void refreshLimits();
@ -92,7 +92,7 @@ export const DeleteDocumentDialog = ({
};
return (
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
<Dialog open={open} onOpenChange={(value) => !isPending && onOpenChange(value)}>
<DialogContent>
<DialogHeader>
<DialogTitle>
@ -193,7 +193,7 @@ export const DeleteDocumentDialog = ({
<Button
type="button"
loading={isLoading}
loading={isPending}
onClick={onDelete}
disabled={!isDeleteEnabled && canManageDocument}
variant="destructive"

View File

@ -48,10 +48,10 @@ export const DuplicateDocumentDialog = ({
const documentsPath = formatDocumentsPath(team?.url);
const { mutateAsync: duplicateDocument, isLoading: isDuplicateLoading } =
const { mutateAsync: duplicateDocument, isPending: isDuplicateLoading } =
trpcReact.document.duplicateDocument.useMutation({
onSuccess: (newId) => {
router.push(`${documentsPath}/${newId}/edit`);
onSuccess: ({ documentId }) => {
router.push(`${documentsPath}/${documentId}/edit`);
toast({
title: _(msg`Document Duplicated`),

View File

@ -42,7 +42,7 @@ export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocum
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
const { mutateAsync: moveDocument, isLoading } = trpc.document.moveDocumentToTeam.useMutation({
const { mutateAsync: moveDocument, isPending } = trpc.document.moveDocumentToTeam.useMutation({
onSuccess: () => {
router.refresh();
toast({
@ -119,8 +119,8 @@ export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocum
<Button variant="secondary" onClick={() => onOpenChange(false)}>
<Trans>Cancel</Trans>
</Button>
<Button onClick={onMove} loading={isLoading} disabled={!selectedTeamId || isLoading}>
{isLoading ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
<Button onClick={onMove} loading={isPending} disabled={!selectedTeamId || isPending}>
{isPending ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
</Button>
</DialogFooter>
</DialogContent>

View File

@ -8,16 +8,16 @@ import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Loader } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { match } from 'ts-pattern';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { AppError } from '@documenso/lib/errors/app-error';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
@ -99,25 +99,20 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
console.error(err);
if (error.code === 'INVALID_DOCUMENT_FILE') {
toast({
title: _(msg`Invalid file`),
description: _(msg`You cannot upload encrypted PDFs`),
variant: 'destructive',
});
} else if (err instanceof TRPCClientError) {
toast({
title: _(msg`Error`),
description: err.message,
variant: 'destructive',
});
} else {
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while uploading your document.`),
variant: 'destructive',
});
}
const errorMessage = match(error.code)
.with('INVALID_DOCUMENT_FILE', () => msg`You cannot upload encrypted PDFs`)
.with(
AppErrorCode.LIMIT_EXCEEDED,
() => msg`You have reached your document limit for this month. Please upgrade your plan.`,
)
.otherwise(() => msg`An error occurred while uploading your document.`);
toast({
title: _(msg`Error`),
description: _(errorMessage),
variant: 'destructive',
duration: 7500,
});
} finally {
setIsLoading(false);
}

View File

@ -36,7 +36,7 @@ export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProp
const [enteredEmail, setEnteredEmail] = useState<string>('');
const { mutateAsync: deleteAccount, isLoading: isDeletingAccount } =
const { mutateAsync: deleteAccount, isPending: isDeletingAccount } =
trpc.profile.deleteAccount.useMutation();
const onDeleteAccount = async () => {

View File

@ -5,7 +5,6 @@ import { useEffect, useMemo, useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import type { FindTemplateRow } from '@documenso/lib/server-only/template/find-templates';
import type {
Team,
TeamProfile,
@ -15,6 +14,7 @@ import type {
} from '@documenso/prisma/client';
import { TemplateType } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import type { FindTemplateRow } from '@documenso/trpc/server/template-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Switch } from '@documenso/ui/primitives/switch';
@ -63,10 +63,10 @@ export const PublicProfilePageView = ({ user, team, profile }: PublicProfilePage
perPage: 100,
});
const { mutateAsync: updateUserProfile, isLoading: isUpdatingUserProfile } =
const { mutateAsync: updateUserProfile, isPending: isUpdatingUserProfile } =
trpc.profile.updatePublicProfile.useMutation();
const { mutateAsync: updateTeamProfile, isLoading: isUpdatingTeamProfile } =
const { mutateAsync: updateTeamProfile, isPending: isUpdatingTeamProfile } =
trpc.team.updateTeamPublicProfile.useMutation();
const isUpdating = isUpdatingUserProfile || isUpdatingTeamProfile;

View File

@ -7,11 +7,11 @@ import { useLingui } from '@lingui/react';
import { EditIcon, FileIcon, LinkIcon, MoreHorizontalIcon, Trash2Icon } from 'lucide-react';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
import type { FindTemplateRow } from '@documenso/lib/server-only/template/find-templates';
import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
import type { TemplateDirectLink } from '@documenso/prisma/client';
import { TemplateType } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import type { FindTemplateRow } from '@documenso/trpc/server/template-router/schema';
import {
DropdownMenu,
DropdownMenuContent,
@ -39,10 +39,10 @@ export const PublicTemplatesDataTable = () => {
templateId: number;
} | null>(null);
const { data, isInitialLoading, isLoadingError, refetch } = trpc.template.findTemplates.useQuery(
const { data, isLoading, isLoadingError, refetch } = trpc.template.findTemplates.useQuery(
{},
{
keepPreviousData: true,
placeholderData: (previousData) => previousData,
},
);
@ -80,7 +80,7 @@ export const PublicTemplatesDataTable = () => {
{/* Loading and error handling states. */}
{publicDirectTemplates.length === 0 && (
<>
{isInitialLoading &&
{isLoading &&
Array(3)
.fill(0)
.map((_, index) => (
@ -115,7 +115,7 @@ export const PublicTemplatesDataTable = () => {
</div>
)}
{!isInitialLoading && (
{!isLoading && (
<div className="text-muted-foreground flex h-32 flex-col items-center justify-center text-sm">
<Trans>No public profile templates found</Trans>
<ManagePublicTemplateDialog

View File

@ -35,16 +35,15 @@ export const UserSecurityActivityDataTable = () => {
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
const { data, isLoading, isInitialLoading, isLoadingError } =
trpc.profile.findUserSecurityAuditLogs.useQuery(
{
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
},
{
keepPreviousData: true,
},
);
const { data, isLoading, isLoadingError } = trpc.profile.findUserSecurityAuditLogs.useQuery(
{
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
},
{
placeholderData: (previousData) => previousData,
},
);
const onPaginationChange = (page: number, perPage: number) => {
updateSearchParams({
@ -134,7 +133,7 @@ export const UserSecurityActivityDataTable = () => {
enable: isLoadingError,
}}
skeleton={{
enable: isLoading && isInitialLoading,
enable: isLoading,
rows: 3,
component: (
<>

View File

@ -65,7 +65,7 @@ export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePass
},
});
const { mutateAsync: createPasskeyRegistrationOptions, isLoading } =
const { mutateAsync: createPasskeyRegistrationOptions, isPending } =
trpc.auth.createPasskeyRegistrationOptions.useMutation();
const { mutateAsync: createPasskey } = trpc.auth.createPasskey.useMutation();
@ -141,7 +141,7 @@ export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePass
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button variant="secondary" loading={isLoading}>
<Button variant="secondary" loading={isPending}>
<KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />
<Trans>Add passkey</Trans>
</Button>

View File

@ -60,7 +60,7 @@ export const UserPasskeysDataTableActions = ({
},
});
const { mutateAsync: updatePasskey, isLoading: isUpdatingPasskey } =
const { mutateAsync: updatePasskey, isPending: isUpdatingPasskey } =
trpc.auth.updatePasskey.useMutation({
onSuccess: () => {
toast({
@ -80,7 +80,7 @@ export const UserPasskeysDataTableActions = ({
},
});
const { mutateAsync: deletePasskey, isLoading: isDeletingPasskey } =
const { mutateAsync: deletePasskey, isPending: isDeletingPasskey } =
trpc.auth.deletePasskey.useMutation({
onSuccess: () => {
toast({

View File

@ -29,13 +29,13 @@ export const UserPasskeysDataTable = () => {
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
const { data, isLoading, isInitialLoading, isLoadingError } = trpc.auth.findPasskeys.useQuery(
const { data, isLoading, isLoadingError } = trpc.auth.findPasskeys.useQuery(
{
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
},
{
keepPreviousData: true,
placeholderData: (previousData) => previousData,
},
);
@ -100,7 +100,7 @@ export const UserPasskeysDataTable = () => {
enable: isLoadingError,
}}
skeleton={{
enable: isLoading && isInitialLoading,
enable: isLoading,
rows: 3,
component: (
<>

View File

@ -17,7 +17,7 @@ export const AcceptTeamInvitationButton = ({ teamId }: AcceptTeamInvitationButto
const {
mutateAsync: acceptTeamInvitation,
isLoading,
isPending,
isSuccess,
} = trpc.team.acceptTeamInvitation.useMutation({
onSuccess: () => {
@ -40,8 +40,8 @@ export const AcceptTeamInvitationButton = ({ teamId }: AcceptTeamInvitationButto
return (
<Button
onClick={async () => acceptTeamInvitation({ teamId })}
loading={isLoading}
disabled={isLoading || isSuccess}
loading={isPending}
disabled={isPending || isSuccess}
>
<Trans>Accept</Trans>
</Button>

View File

@ -17,7 +17,7 @@ export const DeclineTeamInvitationButton = ({ teamId }: DeclineTeamInvitationBut
const {
mutateAsync: declineTeamInvitation,
isLoading,
isPending,
isSuccess,
} = trpc.team.declineTeamInvitation.useMutation({
onSuccess: () => {
@ -40,8 +40,8 @@ export const DeclineTeamInvitationButton = ({ teamId }: DeclineTeamInvitationBut
return (
<Button
onClick={async () => declineTeamInvitation({ teamId })}
loading={isLoading}
disabled={isLoading || isSuccess}
loading={isPending}
disabled={isPending || isSuccess}
variant="ghost"
>
<Trans>Decline</Trans>

View File

@ -30,7 +30,7 @@ export const TeamEmailUsage = ({ teamEmail }: TeamEmailUsageProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { mutateAsync: deleteTeamEmail, isLoading: isDeletingTeamEmail } =
const { mutateAsync: deleteTeamEmail, isPending: isDeletingTeamEmail } =
trpc.team.deleteTeamEmail.useMutation({
onSuccess: () => {
toast({

View File

@ -23,11 +23,11 @@ import { AcceptTeamInvitationButton } from './accept-team-invitation-button';
import { DeclineTeamInvitationButton } from './decline-team-invitation-button';
export const TeamInvitations = () => {
const { data, isInitialLoading } = trpc.team.getTeamInvitations.useQuery();
const { data, isLoading } = trpc.team.getTeamInvitations.useQuery();
return (
<AnimatePresence>
{data && data.length > 0 && !isInitialLoading && (
{data && data.length > 0 && !isLoading && (
<AnimateGenericFadeInOut>
<Alert variant="secondary">
<div className="flex h-full flex-row items-center p-2">

View File

@ -12,7 +12,7 @@ import {
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
SKIP_QUERY_BATCH_META,
} from '@documenso/lib/constants/trpc';
import type { TemplateWithDetails } from '@documenso/prisma/types/template';
import type { TTemplate } from '@documenso/lib/types/template';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -32,7 +32,7 @@ import { useOptionalCurrentTeam } from '~/providers/team';
export type EditTemplateFormProps = {
className?: string;
initialTemplate: TemplateWithDetails;
initialTemplate: TTemplate;
isEnterprise: boolean;
templateRootPath: string;
};
@ -69,7 +69,7 @@ export const EditTemplateForm = ({
},
);
const { Recipient: recipients, Field: fields, templateDocumentData } = template;
const { recipients, fields, templateDocumentData } = template;
const documentFlow: Record<EditTemplateStep, DocumentFlowStep> = {
settings: {

View File

@ -11,7 +11,7 @@ import { Button } from '@documenso/ui/primitives/button';
import { TemplateDirectLinkDialog } from '../template-direct-link-dialog';
export type TemplateDirectLinkDialogWrapperProps = {
template: Template & { directLink?: TemplateDirectLink | null; Recipient: Recipient[] };
template: Template & { directLink?: TemplateDirectLink | null; recipients: Recipient[] };
};
export const TemplateDirectLinkDialogWrapper = ({

View File

@ -69,20 +69,19 @@ export const TemplatePageViewDocumentsTable = ({
Object.fromEntries(searchParams ?? []),
);
const { data, isLoading, isInitialLoading, isLoadingError } =
trpc.document.findDocuments.useQuery(
{
templateId,
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
query: parsedSearchParams.query,
source: parsedSearchParams.source,
status: parsedSearchParams.status,
},
{
keepPreviousData: true,
},
);
const { data, isLoading, isLoadingError } = trpc.document.findDocuments.useQuery(
{
templateId,
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
query: parsedSearchParams.query,
source: parsedSearchParams.source,
status: parsedSearchParams.status,
},
{
placeholderData: (previousData) => previousData,
},
);
const onPaginationChange = (page: number, perPage: number) => {
updateSearchParams({
@ -116,7 +115,7 @@ export const TemplatePageViewDocumentsTable = ({
accessorKey: 'recipient',
cell: ({ row }) => (
<StackAvatarsWithTooltip
recipients={row.original.Recipient}
recipients={row.original.recipients}
documentStatus={row.original.status}
/>
),
@ -241,7 +240,7 @@ export const TemplatePageViewDocumentsTable = ({
enable: isLoadingError,
}}
skeleton={{
enable: isLoading && isInitialLoading,
enable: isLoading,
rows: 3,
component: (
<>

View File

@ -12,7 +12,7 @@ import type { Template, User } from '@documenso/prisma/client';
export type TemplatePageViewInformationProps = {
userId: number;
template: Template & {
User: Pick<User, 'id' | 'name' | 'email'>;
user: Pick<User, 'id' | 'name' | 'email'>;
};
};
@ -28,7 +28,8 @@ export const TemplatePageViewInformation = ({
return [
{
description: msg`Uploaded by`,
value: userId === template.userId ? _(msg`You`) : template.User.name ?? template.User.email,
value:
userId === template.userId ? _(msg`You`) : (template.user.name ?? template.user.email),
},
{
description: msg`Created`,

View File

@ -123,7 +123,7 @@ export const TemplatePageViewRecentActivity = ({
{match(document.source)
.with(DocumentSource.DOCUMENT, DocumentSource.TEMPLATE, () => (
<Trans>
Document created by <span className="font-bold">{document.User.name}</span>
Document created by <span className="font-bold">{document.user.name}</span>
</Trans>
))
.with(DocumentSource.TEMPLATE_DIRECT_LINK, () => (

View File

@ -10,7 +10,7 @@ import { AvatarWithText } from '@documenso/ui/primitives/avatar';
export type TemplatePageViewRecipientsProps = {
template: Template & {
Recipient: Recipient[];
recipients: Recipient[];
};
templateRootPath: string;
};
@ -21,7 +21,7 @@ export const TemplatePageViewRecipients = ({
}: TemplatePageViewRecipientsProps) => {
const { _ } = useLingui();
const recipients = template.Recipient;
const recipients = template.recipients;
return (
<section className="dark:bg-background border-border bg-widget flex flex-col rounded-xl border">

View File

@ -14,6 +14,7 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { DocumentReadOnlyFields } from '~/components/document/document-read-only-fields';
import { TemplateType } from '~/components/formatter/template-type';
import { TemplateBulkSendDialog } from '~/components/templates/template-bulk-send-dialog';
import { DataTableActionDropdown } from '../data-table-action-dropdown';
import { TemplateDirectLinkBadge } from '../template-direct-link-badge';
@ -54,10 +55,10 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
redirect(templateRootPath);
}
const { templateDocumentData, Field, Recipient: recipients, templateMeta } = template;
const { templateDocumentData, fields, recipients, templateMeta } = template;
// Remap to fit the DocumentReadOnlyFields component.
const readOnlyFields = Field.map((field) => {
const readOnlyFields = fields.map((field) => {
const recipient = recipients.find((recipient) => recipient.id === field.recipientId) || {
name: '',
email: '',
@ -66,8 +67,8 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
return {
...field,
Recipient: recipient,
Signature: null,
recipient,
signature: null,
};
});
@ -111,6 +112,8 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
<div className="mt-2 flex flex-row space-x-4 sm:mt-0 sm:self-end">
<TemplateDirectLinkDialogWrapper template={template} />
<TemplateBulkSendDialog templateId={template.id} recipients={template.recipients} />
<Button className="w-full" asChild>
<Link href={`${templateRootPath}/${template.id}/edit`}>
<LucideEdit className="mr-1.5 h-3.5 w-3.5" />
@ -165,7 +168,7 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
<UseTemplateDialog
templateId={template.id}
templateSigningOrder={template.templateMeta?.signingOrder}
recipients={template.Recipient}
recipients={template.recipients}
documentRootPath={documentRootPath}
trigger={
<Button className="w-full">

View File

@ -5,7 +5,7 @@ import { useState } from 'react';
import Link from 'next/link';
import { Trans } from '@lingui/macro';
import { Copy, Edit, MoreHorizontal, MoveRight, Share2Icon, Trash2 } from 'lucide-react';
import { Copy, Edit, MoreHorizontal, MoveRight, Share2Icon, Trash2, Upload } from 'lucide-react';
import { useSession } from 'next-auth/react';
import type { Recipient, Template, TemplateDirectLink } from '@documenso/prisma/client';
@ -17,6 +17,8 @@ import {
DropdownMenuTrigger,
} from '@documenso/ui/primitives/dropdown-menu';
import { TemplateBulkSendDialog } from '~/components/templates/template-bulk-send-dialog';
import { DeleteTemplateDialog } from './delete-template-dialog';
import { DuplicateTemplateDialog } from './duplicate-template-dialog';
import { MoveTemplateDialog } from './move-template-dialog';
@ -25,7 +27,7 @@ import { TemplateDirectLinkDialog } from './template-direct-link-dialog';
export type DataTableActionDropdownProps = {
row: Template & {
directLink?: Pick<TemplateDirectLink, 'token' | 'enabled'> | null;
Recipient: Recipient[];
recipients: Recipient[];
};
templateRootPath: string;
teamId?: number;
@ -86,6 +88,17 @@ export const DataTableActionDropdown = ({
</DropdownMenuItem>
)}
<TemplateBulkSendDialog
templateId={row.id}
recipients={row.recipients}
trigger={
<div className="hover:bg-accent hover:text-accent-foreground relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors">
<Upload className="mr-2 h-4 w-4" />
<Trans>Bulk Send via CSV</Trans>
</div>
}
/>
<DropdownMenuItem
disabled={!isOwner && !isTeamTemplate}
onClick={() => setDeleteDialogOpen(true)}

View File

@ -10,7 +10,7 @@ import { AlertTriangle, Globe2Icon, InfoIcon, Link2Icon, Loader, LockIcon } from
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import type { FindTemplateRow } from '@documenso/lib/server-only/template/find-templates';
import type { FindTemplateRow } from '@documenso/trpc/server/template-router/schema';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
@ -146,7 +146,7 @@ export const TemplatesDataTable = ({
templateId={row.original.id}
templateSigningOrder={row.original.templateMeta?.signingOrder}
documentDistributionMethod={row.original.templateMeta?.distributionMethod}
recipients={row.original.Recipient}
recipients={row.original.recipients}
documentRootPath={documentRootPath}
/>

View File

@ -28,7 +28,7 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD
const { _ } = useLingui();
const { toast } = useToast();
const { mutateAsync: deleteTemplate, isLoading } = trpcReact.template.deleteTemplate.useMutation({
const { mutateAsync: deleteTemplate, isPending } = trpcReact.template.deleteTemplate.useMutation({
onSuccess: () => {
router.refresh();
@ -51,7 +51,7 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD
});
return (
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
<Dialog open={open} onOpenChange={(value) => !isPending && onOpenChange(value)}>
<DialogContent>
<DialogHeader>
<DialogTitle>
@ -70,7 +70,7 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD
<Button
type="button"
variant="secondary"
disabled={isLoading}
disabled={isPending}
onClick={() => onOpenChange(false)}
>
<Trans>Cancel</Trans>
@ -79,7 +79,7 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD
<Button
type="button"
variant="destructive"
loading={isLoading}
loading={isPending}
onClick={async () => deleteTemplate({ templateId: id })}
>
<Trans>Delete</Trans>

View File

@ -32,7 +32,7 @@ export const DuplicateTemplateDialog = ({
const { _ } = useLingui();
const { toast } = useToast();
const { mutateAsync: duplicateTemplate, isLoading } =
const { mutateAsync: duplicateTemplate, isPending } =
trpcReact.template.duplicateTemplate.useMutation({
onSuccess: () => {
router.refresh();
@ -55,7 +55,7 @@ export const DuplicateTemplateDialog = ({
});
return (
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
<Dialog open={open} onOpenChange={(value) => !isPending && onOpenChange(value)}>
<DialogContent>
<DialogHeader>
<DialogTitle>
@ -70,7 +70,7 @@ export const DuplicateTemplateDialog = ({
<DialogFooter>
<Button
type="button"
disabled={isLoading}
disabled={isPending}
variant="secondary"
onClick={() => onOpenChange(false)}
>
@ -79,7 +79,7 @@ export const DuplicateTemplateDialog = ({
<Button
type="button"
loading={isLoading}
loading={isPending}
onClick={async () =>
duplicateTemplate({
templateId: id,

View File

@ -43,7 +43,7 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
const { mutateAsync: moveTemplate, isLoading } = trpc.template.moveTemplateToTeam.useMutation({
const { mutateAsync: moveTemplate, isPending } = trpc.template.moveTemplateToTeam.useMutation({
onSuccess: () => {
router.refresh();
toast({
@ -130,8 +130,8 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
<Button variant="secondary" onClick={() => onOpenChange(false)}>
<Trans>Cancel</Trans>
</Button>
<Button onClick={onMove} loading={isLoading} disabled={!selectedTeamId || isLoading}>
{isLoading ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
<Button onClick={onMove} loading={isPending} disabled={!selectedTeamId || isPending}>
{isPending ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
</Button>
</DialogFooter>
</DialogContent>

View File

@ -46,12 +46,10 @@ import {
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
type TemplateDirectLinkDialogProps = {
template: Template & {
directLink?: Pick<TemplateDirectLink, 'token' | 'enabled'> | null;
Recipient: Recipient[];
recipients: Recipient[];
};
open: boolean;
onOpenChange: (_open: boolean) => void;
@ -68,8 +66,6 @@ export const TemplateDirectLinkDialog = ({
const { quota, remaining } = useLimits();
const { _ } = useLingui();
const team = useOptionalCurrentTeam();
const [, copy] = useCopyToClipboard();
const router = useRouter();
@ -81,13 +77,13 @@ export const TemplateDirectLinkDialog = ({
);
const validDirectTemplateRecipients = useMemo(
() => template.Recipient.filter((recipient) => recipient.role !== RecipientRole.CC),
[template.Recipient],
() => template.recipients.filter((recipient) => recipient.role !== RecipientRole.CC),
[template.recipients],
);
const {
mutateAsync: createTemplateDirectLink,
isLoading: isCreatingTemplateDirectLink,
isPending: isCreatingTemplateDirectLink,
reset: resetCreateTemplateDirectLink,
} = trpcReact.template.createTemplateDirectLink.useMutation({
onSuccess: (data) => {
@ -108,7 +104,7 @@ export const TemplateDirectLinkDialog = ({
},
});
const { mutateAsync: toggleTemplateDirectLink, isLoading: isTogglingTemplateAccess } =
const { mutateAsync: toggleTemplateDirectLink, isPending: isTogglingTemplateAccess } =
trpcReact.template.toggleTemplateDirectLink.useMutation({
onSuccess: (data) => {
const enabledDescription = msg`Direct link signing has been enabled`;
@ -131,7 +127,7 @@ export const TemplateDirectLinkDialog = ({
},
});
const { mutateAsync: deleteTemplateDirectLink, isLoading: isDeletingTemplateDirectLink } =
const { mutateAsync: deleteTemplateDirectLink, isPending: isDeletingTemplateDirectLink } =
trpcReact.template.deleteTemplateDirectLink.useMutation({
onSuccess: () => {
onOpenChange(false);
@ -326,7 +322,7 @@ export const TemplateDirectLinkDialog = ({
</div>
{/* Prevent creating placeholder direct template recipient if the email already exists. */}
{!template.Recipient.some(
{!template.recipients.some(
(recipient) => recipient.email === DIRECT_TEMPLATE_RECIPIENT_EMAIL,
) && (
<DialogFooter className="mx-auto">

View File

@ -104,7 +104,7 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
<span className="font-medium">{_(msg`Owner`)}</span>
<span className="mt-1 block break-words">
{document.User.name} ({document.User.email})
{document.user.name} ({document.user.email})
</span>
</p>
@ -140,7 +140,7 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
<p className="font-medium">{_(msg`Recipients`)}</p>
<ul className="mt-1 list-inside list-disc">
{document.Recipient.map((recipient) => (
{document.recipients.map((recipient) => (
<li key={recipient.id}>
<span className="text-muted-foreground">
[{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}]

View File

@ -1,5 +1,3 @@
import React from 'react';
import { redirect } from 'next/navigation';
import { msg } from '@lingui/macro';
@ -7,8 +5,10 @@ import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
import { UAParser } from 'ua-parser-js';
import { renderSVG } from 'uqr';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { APP_I18N_OPTIONS, ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n';
import {
RECIPIENT_ROLES_DESCRIPTION,
@ -73,6 +73,8 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
id: documentId,
}).catch(() => null);
const documentAccessToken = document?.documentAccessToken?.token;
if (!document) {
return redirect('/');
}
@ -86,7 +88,7 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
});
const isOwner = (email: string) => {
return email.toLowerCase() === document.User.email.toLowerCase();
return email.toLowerCase() === document.user.email.toLowerCase();
};
const getDevice = (userAgent?: string | null) => {
@ -104,7 +106,7 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
};
const getAuthenticationLevel = (recipientId: number) => {
const recipient = document.Recipient.find((recipient) => recipient.id === recipientId);
const recipient = document.recipients.find((recipient) => recipient.id === recipientId);
if (!recipient) {
return 'Unknown';
@ -157,9 +159,11 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
};
const getRecipientSignatureField = (recipientId: number) => {
return document.Recipient.find((recipient) => recipient.id === recipientId)?.Field.find(
(field) => field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE,
);
return document.recipients
.find((recipient) => recipient.id === recipientId)
?.fields.find(
(field) => field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE,
);
};
return (
@ -181,7 +185,7 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
</TableHeader>
<TableBody className="print:text-xs">
{document.Recipient.map((recipient, i) => {
{document.recipients.map((recipient, i) => {
const logs = getRecipientAuditLogs(recipient.id);
const signature = getRecipientSignatureField(recipient.id);
@ -209,17 +213,17 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
boxShadow: `0px 0px 0px 4.88px rgba(122, 196, 85, 0.1), 0px 0px 0px 1.22px rgba(122, 196, 85, 0.6), 0px 0px 0px 0.61px rgba(122, 196, 85, 1)`,
}}
>
{signature.Signature?.signatureImageAsBase64 && (
{signature.signature?.signatureImageAsBase64 && (
<img
src={`${signature.Signature?.signatureImageAsBase64}`}
src={`${signature.signature?.signatureImageAsBase64}`}
alt="Signature"
className="max-h-12 max-w-full"
/>
)}
{signature.Signature?.typedSignature && (
{signature.signature?.typedSignature && (
<p className="font-signature text-center text-sm">
{signature.Signature?.typedSignature}
{signature.signature?.typedSignature}
</p>
)}
</div>
@ -303,17 +307,27 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
</TableBody>
</Table>
</CardContent>
</Card>
<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 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>
<div>
<p className="flex-shrink-0 text-sm print:text-xs">
{_(msg`Signing certificate provided by`)}:
</p>
<Logo className="max-h-6 print:max-h-4" />
<Logo className="mt-2 max-h-6 print:max-h-4" />
</div>
</div>
</div>
</Card>
</div>
);
}

View File

@ -7,8 +7,9 @@ import { useSession } from 'next-auth/react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import type { Field, Recipient } from '@documenso/prisma/client';
import type { TemplateWithDetails } from '@documenso/prisma/types/template';
import type { TTemplate } from '@documenso/lib/types/template';
import type { Recipient } from '@documenso/prisma/client';
import type { Field } from '@documenso/prisma/client';
import {
DocumentFlowFormContainerActions,
DocumentFlowFormContainerContent,
@ -40,8 +41,8 @@ export type TConfigureDirectTemplateFormSchema = z.infer<typeof ZConfigureDirect
export type ConfigureDirectTemplateFormProps = {
flowStep: DocumentFlowStep;
isDocumentPdfLoaded: boolean;
template: Omit<TemplateWithDetails, 'User'>;
directTemplateRecipient: Recipient & { Field: Field[] };
template: Omit<TTemplate, 'user'>;
directTemplateRecipient: Recipient & { fields: Field[] };
initialEmail?: string;
onSubmit: (_data: TConfigureDirectTemplateFormSchema) => void;
};
@ -57,10 +58,10 @@ export const ConfigureDirectTemplateFormPartial = ({
const { _ } = useLingui();
const { data: session } = useSession();
const { Recipient } = template;
const { recipients } = template;
const { derivedRecipientAccessAuth } = useRequiredDocumentAuthContext();
const recipientsWithBlankDirectRecipientEmail = Recipient.map((recipient) => {
const recipientsWithBlankDirectRecipientEmail = recipients.map((recipient) => {
if (recipient.id === directTemplateRecipient.id) {
return {
...recipient,
@ -74,7 +75,7 @@ export const ConfigureDirectTemplateFormPartial = ({
const form = useForm<TConfigureDirectTemplateFormSchema>({
resolver: zodResolver(
ZConfigureDirectTemplateFormSchema.superRefine((items, ctx) => {
if (template.Recipient.map((recipient) => recipient.email).includes(items.email)) {
if (template.recipients.map((recipient) => recipient.email).includes(items.email)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: _(msg`Email cannot already exist in the template`),
@ -96,7 +97,7 @@ export const ConfigureDirectTemplateFormPartial = ({
<DocumentFlowFormContainerContent>
{isDocumentPdfLoaded &&
directTemplateRecipient.Field.map((field, index) => (
directTemplateRecipient.fields.map((field, index) => (
<ShowFieldItem
key={index}
field={field}

View File

@ -8,9 +8,9 @@ import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import type { TTemplate } from '@documenso/lib/types/template';
import type { Field } from '@documenso/prisma/client';
import { type Recipient } from '@documenso/prisma/client';
import type { TemplateWithDetails } from '@documenso/prisma/types/template';
import { trpc } from '@documenso/trpc/react';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
@ -28,9 +28,9 @@ import type { DirectTemplateLocalField } from './sign-direct-template';
import { SignDirectTemplateForm } from './sign-direct-template';
export type TemplatesDirectPageViewProps = {
template: Omit<TemplateWithDetails, 'User'>;
template: Omit<TTemplate, 'user'>;
directTemplateToken: string;
directTemplateRecipient: Recipient & { Field: Field[] };
directTemplateRecipient: Recipient & { fields: Field[] };
};
type DirectTemplateStep = 'configure' | 'sign';
@ -164,7 +164,7 @@ export const DirectTemplatePageView = ({
<SignDirectTemplateForm
flowStep={directTemplateFlow.sign}
directRecipient={recipient}
directRecipientFields={directTemplateRecipient.Field}
directRecipientFields={directTemplateRecipient.fields}
template={template}
onSubmit={onSignDirectTemplateSubmit}
/>

View File

@ -41,7 +41,7 @@ export default async function TemplatesDirectPage({ params }: TemplatesDirectPag
notFound();
}
const directTemplateRecipient = template.Recipient.find(
const directTemplateRecipient = template.recipients.find(
(recipient) => recipient.id === template.directLink?.directTemplateRecipientId,
);
@ -81,7 +81,7 @@ export default async function TemplatesDirectPage({ params }: TemplatesDirectPag
<div className="text-muted-foreground mb-8 mt-2.5 flex items-center gap-x-2">
<UsersIcon className="h-4 w-4" />
<p className="text-muted-foreground/80">
<Plural value={template.Recipient.length} one="# recipient" other="# recipients" />
<Plural value={template.recipients.length} one="# recipient" other="# recipients" />
</p>
</div>

View File

@ -14,10 +14,10 @@ import {
ZRadioFieldMeta,
ZTextFieldMeta,
} from '@documenso/lib/types/field-meta';
import type { TTemplate } from '@documenso/lib/types/template';
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { Field, Recipient, Signature } from '@documenso/prisma/client';
import { FieldType } from '@documenso/prisma/client';
import type { TemplateWithDetails } from '@documenso/prisma/types/template';
import type {
TRemovedSignedFieldWithTokenMutationSchema,
TSignFieldWithTokenMutationSchema,
@ -55,13 +55,13 @@ export type SignDirectTemplateFormProps = {
flowStep: DocumentFlowStep;
directRecipient: Recipient;
directRecipientFields: Field[];
template: Omit<TemplateWithDetails, 'User'>;
template: Omit<TTemplate, 'user'>;
onSubmit: (_data: DirectTemplateLocalField[]) => Promise<void>;
};
export type DirectTemplateLocalField = Field & {
signedValue?: TSignFieldWithTokenMutationSchema;
Signature?: Signature;
signature?: Signature;
};
export const SignDirectTemplateForm = ({
@ -95,7 +95,7 @@ export const SignDirectTemplateForm = ({
};
if (field.type === FieldType.SIGNATURE) {
tempField.Signature = {
tempField.signature = {
id: 1,
created: new Date(),
recipientId: 1,
@ -127,7 +127,7 @@ export const SignDirectTemplateForm = ({
customText: '',
inserted: false,
signedValue: undefined,
Signature: undefined,
signature: undefined,
};
}),
);

View File

@ -0,0 +1,44 @@
'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

@ -0,0 +1,35 @@
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

@ -0,0 +1,68 @@
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

@ -52,15 +52,15 @@ export async function GET(_request: Request, { params: { slug } }: SharePageOpen
const isRecipient = 'Signature' in recipientOrSender;
const signatureImage = match(recipientOrSender)
.with({ Signature: P.array(P._) }, (recipient) => {
return recipient.Signature?.[0]?.signatureImageAsBase64 || null;
.with({ signatures: P.array(P._) }, (recipient) => {
return recipient.signatures?.[0]?.signatureImageAsBase64 || null;
})
.otherwise((sender) => {
return sender.signature || null;
});
const signatureName = match(recipientOrSender)
.with({ Signature: P.array(P._) }, (recipient) => {
.with({ signatures: P.array(P._) }, (recipient) => {
return recipient.name || recipient.email;
})
.otherwise((sender) => {

View File

@ -81,12 +81,12 @@ export const CheckboxField = ({
);
}, [checkedValues, validationSign, checkboxValidationLength]);
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } =
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const {
mutateAsync: removeSignedFieldWithToken,
isLoading: isRemoveSignedFieldWithTokenLoading,
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;

View File

@ -16,6 +16,7 @@ import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZDateFieldMeta } from '@documenso/lib/types/field-meta';
import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react';
@ -23,6 +24,7 @@ import type {
TRemovedSignedFieldWithTokenMutationSchema,
TSignFieldWithTokenMutationSchema,
} from '@documenso/trpc/server/field-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { SigningFieldContainer } from './signing-field-container';
@ -51,14 +53,17 @@ export const DateField = ({
const [isPending, startTransition] = useTransition();
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } =
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const {
mutateAsync: removeSignedFieldWithToken,
isLoading: isRemoveSignedFieldWithTokenLoading,
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const safeFieldMeta = ZDateFieldMeta.safeParse(field.fieldMeta);
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
const localDateString = convertToLocalSystemFormat(field.customText, dateFormat, timezone);
@ -150,9 +155,21 @@ export const DateField = ({
)}
{field.inserted && (
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.425rem,25cqw,0.825rem)] duration-200">
{localDateString}
</p>
<div className="flex h-full w-full items-center">
<p
className={cn(
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
{
'text-left': parsedFieldMeta?.textAlign === 'left',
'text-center':
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>
{localDateString}
</p>
</div>
)}
</SigningFieldContainer>
);

View File

@ -106,7 +106,7 @@ export const DocumentAuthProvider = ({
perPage: MAXIMUM_PASSKEYS,
},
{
keepPreviousData: true,
placeholderData: (previousData) => previousData,
enabled: derivedRecipientActionAuth === DocumentAuth.PASSKEY,
},
);

View File

@ -58,12 +58,12 @@ export const DropdownField = ({
const defaultValue = parsedFieldMeta?.defaultValue;
const [localChoice, setLocalChoice] = useState(parsedFieldMeta.defaultValue ?? '');
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } =
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const {
mutateAsync: removeSignedFieldWithToken,
isLoading: isRemoveSignedFieldWithTokenLoading,
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;

View File

@ -11,6 +11,7 @@ import { Loader } from 'lucide-react';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZEmailFieldMeta } from '@documenso/lib/types/field-meta';
import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react';
@ -18,6 +19,7 @@ import type {
TRemovedSignedFieldWithTokenMutationSchema,
TSignFieldWithTokenMutationSchema,
} from '@documenso/trpc/server/field-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredSigningContext } from './provider';
@ -40,14 +42,17 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
const [isPending, startTransition] = useTransition();
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } =
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const {
mutateAsync: removeSignedFieldWithToken,
isLoading: isRemoveSignedFieldWithTokenLoading,
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const safeFieldMeta = ZEmailFieldMeta.safeParse(field.fieldMeta);
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
const onSign = async (authOptions?: TRecipientActionAuth) => {
@ -128,9 +133,21 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
)}
{field.inserted && (
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.425rem,25cqw,0.825rem)] duration-200">
{field.customText}
</p>
<div className="flex h-full w-full items-center">
<p
className={cn(
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
{
'text-left': parsedFieldMeta?.textAlign === 'left',
'text-center':
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>
{field.customText}
</p>
</div>
)}
</SigningFieldContainer>
);

View File

@ -46,12 +46,12 @@ export const InitialsField = ({
const [isPending, startTransition] = useTransition();
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } =
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const {
mutateAsync: removeSignedFieldWithToken,
isLoading: isRemoveSignedFieldWithTokenLoading,
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;

View File

@ -11,6 +11,7 @@ import { Loader } from 'lucide-react';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZNameFieldMeta } from '@documenso/lib/types/field-meta';
import { type Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react';
@ -18,6 +19,7 @@ import type {
TRemovedSignedFieldWithTokenMutationSchema,
TSignFieldWithTokenMutationSchema,
} from '@documenso/trpc/server/field-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
@ -48,14 +50,17 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
const [isPending, startTransition] = useTransition();
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } =
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const {
mutateAsync: removeSignedFieldWithToken,
isLoading: isRemoveSignedFieldWithTokenLoading,
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const safeFieldMeta = ZNameFieldMeta.safeParse(field.fieldMeta);
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
const [showFullNameModal, setShowFullNameModal] = useState(false);
@ -172,9 +177,21 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
)}
{field.inserted && (
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.425rem,25cqw,0.825rem)] duration-200">
{field.customText}
</p>
<div className="flex h-full w-full items-center">
<p
className={cn(
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
{
'text-left': parsedFieldMeta?.textAlign === 'left',
'text-center':
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>
{field.customText}
</p>
</div>
)}
<Dialog open={showFullNameModal} onOpenChange={setShowFullNameModal}>

View File

@ -52,8 +52,19 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
const [isPending, startTransition] = useTransition();
const [showRadioModal, setShowRadioModal] = useState(false);
const parsedFieldMeta = field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null;
const isReadOnly = parsedFieldMeta?.readOnly;
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const {
mutateAsync: removeSignedFieldWithToken,
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const safeFieldMeta = ZNumberFieldMeta.safeParse(field.fieldMeta);
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
const defaultValue = parsedFieldMeta?.value;
const [localNumber, setLocalNumber] = useState(
parsedFieldMeta?.value ? String(parsedFieldMeta.value) : '0',
@ -71,16 +82,6 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const {
mutateAsync: removeSignedFieldWithToken,
isLoading: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
const handleNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const text = e.target.value;
setLocalNumber(text);
@ -208,7 +209,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
useEffect(() => {
if (
(!field.inserted && defaultValue && localNumber) ||
(!field.inserted && isReadOnly && defaultValue)
(!field.inserted && parsedFieldMeta?.readOnly && defaultValue)
) {
void executeActionAuthProcedure({
onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
@ -260,9 +261,21 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
)}
{field.inserted && (
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.425rem,25cqw,0.825rem)] duration-200">
{field.customText}
</p>
<div className="flex h-full w-full items-center">
<p
className={cn(
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
{
'text-left': parsedFieldMeta?.textAlign === 'left',
'text-center':
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>
{field.customText}
</p>
</div>
)}
<Dialog open={showRadioModal} onOpenChange={setShowRadioModal}>

View File

@ -52,12 +52,12 @@ export const RadioField = ({ field, recipient, onSignField, onUnsignField }: Rad
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } =
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const {
mutateAsync: removeSignedFieldWithToken,
isLoading: isRemoveSignedFieldWithTokenLoading,
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;

View File

@ -66,15 +66,15 @@ export const SignatureField = ({
const [isPending, startTransition] = useTransition();
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } =
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const {
mutateAsync: removeSignedFieldWithToken,
isLoading: isRemoveSignedFieldWithTokenLoading,
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const { Signature: signature } = field;
const { signature } = field;
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;

View File

@ -56,8 +56,8 @@ export const SigningPageView = ({
const shouldUseTeamDetails =
document.teamId && document.team?.teamGlobalSettings?.includeSenderDetails === false;
let senderName = document.User.name ?? '';
let senderEmail = `(${document.User.email})`;
let senderName = document.user.name ?? '';
let senderEmail = `(${document.user.email})`;
if (shouldUseTeamDetails) {
senderName = document.team?.name ?? '';

View File

@ -54,15 +54,16 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
const [isPending, startTransition] = useTransition();
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } =
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const {
mutateAsync: removeSignedFieldWithToken,
isLoading: isRemoveSignedFieldWithTokenLoading,
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const parsedFieldMeta = field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : null;
const safeFieldMeta = ZTextFieldMeta.safeParse(field.fieldMeta);
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
const shouldAutoSignField =
@ -261,11 +262,23 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
)}
{field.inserted && (
<p className="text-muted-foreground dark:text-background/80 flex items-center justify-center gap-x-1 text-[clamp(0.425rem,25cqw,0.825rem)] duration-200">
{field.customText.length < 20
? field.customText
: field.customText.substring(0, 15) + '...'}
</p>
<div className="flex h-full w-full items-center">
<p
className={cn(
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
{
'text-left': parsedFieldMeta?.textAlign === 'left',
'text-center':
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>
{field.customText.length < 20
? field.customText
: field.customText.substring(0, 15) + '...'}
</p>
</div>
)}
<Dialog open={showCustomTextModal} onOpenChange={setShowCustomTextModal}>
@ -281,6 +294,10 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
className={cn('mt-2 w-full rounded-md', {
'border-2 border-red-300 ring-2 ring-red-200 ring-offset-2 ring-offset-red-200 focus-visible:border-red-400 focus-visible:ring-4 focus-visible:ring-red-200 focus-visible:ring-offset-2 focus-visible:ring-offset-red-200':
userInputHasErrors,
'text-left': parsedFieldMeta?.textAlign === 'left',
'text-center':
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right',
})}
value={localText}
onChange={handleTextChange}

View File

@ -38,7 +38,7 @@ export const LayoutBillingBanner = ({
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync: createBillingPortal, isLoading } =
const { mutateAsync: createBillingPortal, isPending } =
trpc.team.createBillingPortal.useMutation();
const handleCreatePortal = async () => {
@ -92,7 +92,7 @@ export const LayoutBillingBanner = ({
'text-destructive-foreground hover:bg-destructive-foreground hover:text-white':
subscription.status === SubscriptionStatus.INACTIVE,
})}
disabled={isLoading}
disabled={isPending}
onClick={() => setIsOpen(true)}
size="sm"
>
@ -101,7 +101,7 @@ export const LayoutBillingBanner = ({
</div>
</div>
<Dialog open={isOpen} onOpenChange={(value) => !isLoading && setIsOpen(value)}>
<Dialog open={isOpen} onOpenChange={(value) => !isPending && setIsOpen(value)}>
<DialogContent>
<DialogTitle>
<Trans>Payment overdue</Trans>
@ -128,7 +128,7 @@ export const LayoutBillingBanner = ({
{canExecuteTeamAction('MANAGE_BILLING', userRole) && (
<DialogFooter>
<Button loading={isLoading} onClick={handleCreatePortal}>
<Button loading={isPending} onClick={handleCreatePortal}>
<Trans>Resolve payment</Trans>
</Button>
</DialogFooter>

View File

@ -25,7 +25,7 @@ export const TeamEmailDropdown = ({ team }: TeamsSettingsPageProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { mutateAsync: resendEmailVerification, isLoading: isResendingEmailVerification } =
const { mutateAsync: resendEmailVerification, isPending: isResendingEmailVerification } =
trpc.team.resendTeamEmailVerification.useMutation({
onSuccess: () => {
toast({

View File

@ -36,7 +36,7 @@ export const TeamTransferStatus = ({
const isExpired = transferVerification && isTokenExpired(transferVerification.expiresAt);
const { mutateAsync: deleteTeamTransferRequest, isLoading } =
const { mutateAsync: deleteTeamTransferRequest, isPending } =
trpc.team.deleteTeamTransferRequest.useMutation({
onSuccess: () => {
if (!isExpired) {
@ -112,7 +112,7 @@ export const TeamTransferStatus = ({
{canExecuteTeamAction('DELETE_TEAM_TRANSFER_REQUEST', currentUserTeamRole) && (
<Button
onClick={async () => deleteTeamTransferRequest({ teamId })}
loading={isLoading}
loading={isPending}
variant={isExpired ? 'destructive' : 'ghost'}
className={cn('ml-auto', {
'hover:bg-transparent hover:text-blue-800': !isExpired,

View File

@ -100,7 +100,7 @@ export const EmbedDirectTemplateClientPage = ({
const hasSignatureField = localFields.some((field) => field.type === FieldType.SIGNATURE);
const { mutateAsync: createDocumentFromDirectTemplate, isLoading: isSubmitting } =
const { mutateAsync: createDocumentFromDirectTemplate, isPending: isSubmitting } =
trpc.template.createDocumentFromDirectTemplate.useMutation();
const onSignField = (payload: TSignFieldWithTokenMutationSchema) => {
@ -118,7 +118,7 @@ export const EmbedDirectTemplateClientPage = ({
});
if (field.type === FieldType.SIGNATURE) {
newField.Signature = {
newField.signature = {
id: 1,
created: new Date(),
recipientId: 1,
@ -163,7 +163,7 @@ export const EmbedDirectTemplateClientPage = ({
customText: '',
inserted: false,
signedValue: undefined,
Signature: undefined,
signature: undefined,
});
}),
);

View File

@ -73,7 +73,7 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem
const { directTemplateRecipientId } = template.directLink;
const recipient = template.Recipient.find(
const recipient = template.recipients.find(
(recipient) => recipient.id === directTemplateRecipientId,
);
@ -81,7 +81,7 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem
return notFound();
}
const fields = template.Field.filter((field) => field.recipientId === directTemplateRecipientId);
const fields = template.fields.filter((field) => field.recipientId === directTemplateRecipientId);
const team = template.teamId
? await getTeamById({ teamId: template.teamId, userId: template.userId }).catch(() => null)

View File

@ -84,7 +84,7 @@ export const EmbedSignDocumentClientPage = ({
fields.filter((field) => field.inserted),
];
const { mutateAsync: completeDocumentWithToken, isLoading: isSubmitting } =
const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } =
trpc.recipient.completeDocumentWithToken.useMutation();
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);

View File

@ -91,7 +91,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
query: search,
},
{
keepPreviousData: true,
placeholderData: (previousData) => previousData,
// Do not batch this due to relatively long request time compared to
// other queries which are generally batched with this.
...SKIP_QUERY_BATCH_META,

View File

@ -31,7 +31,7 @@ export const VerifyEmailBanner = ({ email }: VerifyEmailBannerProps) => {
const [isButtonDisabled, setIsButtonDisabled] = useState(false);
const { mutateAsync: sendConfirmationEmail, isLoading } =
const { mutateAsync: sendConfirmationEmail, isPending } =
trpc.profile.sendConfirmationEmail.useMutation();
const onResendConfirmationEmail = async () => {
@ -122,10 +122,10 @@ export const VerifyEmailBanner = ({ email }: VerifyEmailBannerProps) => {
<div>
<Button
disabled={isButtonDisabled}
loading={isLoading}
loading={isPending}
onClick={onResendConfirmationEmail}
>
{isLoading ? <Trans>Sending...</Trans> : <Trans>Resend Confirmation Email</Trans>}
{isPending ? <Trans>Sending...</Trans> : <Trans>Resend Confirmation Email</Trans>}
</Button>
</div>
</DialogContent>

View File

@ -64,7 +64,7 @@ export const AddTeamEmailDialog = ({ teamId, trigger, ...props }: AddTeamEmailDi
},
});
const { mutateAsync: createTeamEmailVerification, isLoading } =
const { mutateAsync: createTeamEmailVerification, isPending } =
trpc.team.createTeamEmailVerification.useMutation();
const onFormSubmit = async ({ name, email }: TCreateTeamEmailFormSchema) => {
@ -120,7 +120,7 @@ export const AddTeamEmailDialog = ({ teamId, trigger, ...props }: AddTeamEmailDi
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button variant="outline" loading={isLoading} className="bg-background">
<Button variant="outline" loading={isPending} className="bg-background">
<Plus className="-ml-1 mr-1 h-5 w-5" />
<Trans>Add email</Trans>
</Button>

View File

@ -39,7 +39,7 @@ export const CreateTeamCheckoutDialog = ({
const { data, isLoading } = trpc.team.getTeamPrices.useQuery();
const { mutateAsync: createCheckout, isLoading: isCreatingCheckout } =
const { mutateAsync: createCheckout, isPending: isCreatingCheckout } =
trpc.team.createTeamPendingCheckout.useMutation({
onSuccess: (checkoutUrl) => {
window.open(checkoutUrl, '_blank');

View File

@ -42,7 +42,7 @@ export const DeleteTeamMemberDialog = ({
const { _ } = useLingui();
const { toast } = useToast();
const { mutateAsync: deleteTeamMembers, isLoading: isDeletingTeamMember } =
const { mutateAsync: deleteTeamMembers, isPending: isDeletingTeamMember } =
trpc.team.deleteTeamMembers.useMutation({
onSuccess: () => {
toast({

View File

@ -43,7 +43,7 @@ export const LeaveTeamDialog = ({
const { _ } = useLingui();
const { toast } = useToast();
const { mutateAsync: leaveTeam, isLoading: isLeavingTeam } = trpc.team.leaveTeam.useMutation({
const { mutateAsync: leaveTeam, isPending: isLeavingTeam } = trpc.team.leaveTeam.useMutation({
onSuccess: () => {
toast({
title: _(msg`Success`),

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