Compare commits

...

34 Commits

Author SHA1 Message Date
8d85d01c93 chore: remove unused type 2025-01-30 05:20:38 +00:00
cdb980bd4c chore: avoid using lastSignedIn 2025-01-29 07:25:07 +00:00
413c3cbc6e Merge branch 'main' into feat/mau 2025-01-29 00:30:43 +00:00
ff4ba71ef3 chore: add tooltip 2025-01-29 00:25:07 +00:00
9bc825b308 chore: cleanup 2025-01-28 16:19:14 +00:00
d7f7cbf27f fix: exclude disabled users 2025-01-28 15:57:15 +00:00
97f7d77a34 chore: cleanup 2025-01-28 15:40:40 +00:00
6993c52b2a feat: mau 2025-01-28 15:29:36 +00: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
68c8eba2c3 chore: wip 2025-01-28 05:08:28 +00: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
c40e802396 feat: add mau card 2025-01-25 12:23:26 +00: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
299 changed files with 7376 additions and 5214 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

@ -27,6 +27,6 @@
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"typescript": "^5"
"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

@ -16,8 +16,8 @@
"next": "14.2.6"
},
"devDependencies": {
"@types/node": "20.16.5",
"@types/node": "^20",
"@types/react": "18.3.5",
"typescript": "5.5.4"
"typescript": "5.6.2"
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@documenso/web",
"version": "1.9.0-rc.8",
"version": "1.9.0-rc.11",
"private": true,
"license": "AGPL-3.0",
"scripts": {
@ -26,7 +26,6 @@
"@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",
@ -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",
@ -68,11 +67,11 @@
"@simplewebauthn/types": "^9.0.1",
"@types/formidable": "^2.0.6",
"@types/luxon": "^3.3.1",
"@types/node": "20.1.0",
"@types/node": "^20",
"@types/papaparse": "^5.3.14",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/ua-parser-js": "^0.7.39",
"typescript": "5.2.2"
"typescript": "5.6.2"
}
}

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

@ -0,0 +1,73 @@
'use client';
import { DateTime } from 'luxon';
import type { TooltipProps } from 'recharts';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import type { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent';
import type { GetMonthlyActiveUsersResult } from '@documenso/lib/server-only/admin/get-users-stats';
export type MonthlyActiveUsersChartProps = {
className?: string;
title: string;
cummulative?: boolean;
data: GetMonthlyActiveUsersResult;
};
const CustomTooltip = ({ active, payload, label }: TooltipProps<ValueType, NameType>) => {
if (active && payload && payload.length) {
return (
<div className="z-100 w-60 space-y-1 rounded-md border border-solid bg-white p-2 px-3">
<p>{label}</p>
<p className="text-documenso">
{payload[0].name === 'cume_count' ? 'Cumulative MAU' : 'Monthly Active Users'}:{' '}
<span className="text-black">{Number(payload[0].value).toLocaleString('en-US')}</span>
</p>
</div>
);
}
return null;
};
export const MonthlyActiveUsersChart = ({
className,
data,
title,
cummulative = false,
}: MonthlyActiveUsersChartProps) => {
const formattedData = [...data].reverse().map(({ month, count, cume_count }) => {
return {
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('MMM yyyy'),
count: Number(count),
cume_count: Number(cume_count),
};
});
return (
<div className={className}>
<div className="border-border flex flex-1 flex-col justify-center rounded-2xl border p-6 pl-2">
<div className="mb-6 flex px-4">
<h3 className="text-lg font-semibold">{title}</h3>
</div>
<ResponsiveContainer width="100%" height={400}>
<BarChart data={formattedData}>
<XAxis dataKey="month" />
<YAxis />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'hsl(var(--primary) / 10%)' }} />
<Bar
dataKey={cummulative ? 'cume_count' : 'count'}
fill="hsl(var(--primary))"
radius={[4, 4, 0, 0]}
maxBarSize={60}
label={cummulative ? 'Cumulative MAU' : 'Monthly Active Users'}
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
);
};

View File

@ -18,6 +18,7 @@ import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats';
import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats';
import {
getMonthlyActiveUsers,
getUserWithSignedDocumentMonthlyGrowth,
getUsersCount,
getUsersWithSubscriptionsCount,
@ -26,6 +27,7 @@ import { getSignerConversionMonthly } from '@documenso/lib/server-only/user/get-
import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card';
import { MonthlyActiveUsersChart } from './monthly-active-users-chart';
import { SignerConversionChart } from './signer-conversion-chart';
import { UserWithDocumentChart } from './user-with-document';
@ -43,6 +45,7 @@ export default async function AdminStatsPage() {
// userWithAtLeastOneDocumentPerMonth,
// userWithAtLeastOneDocumentSignedPerMonth,
MONTHLY_USERS_SIGNED,
MONTHLY_ACTIVE_USERS,
] = await Promise.all([
getUsersCount(),
getUsersWithSubscriptionsCount(),
@ -52,6 +55,7 @@ export default async function AdminStatsPage() {
// getUserWithAtLeastOneDocumentPerMonth(),
// getUserWithAtLeastOneDocumentSignedPerMonth(),
getUserWithSignedDocumentMonthlyGrowth(),
getMonthlyActiveUsers(),
]);
return (
@ -68,7 +72,6 @@ export default async function AdminStatsPage() {
title={_(msg`Active Subscriptions`)}
value={usersWithSubscriptionsCount}
/>
<CardMetric
icon={FileCog}
title={_(msg`App Version`)}
@ -132,6 +135,14 @@ export default async function AdminStatsPage() {
<Trans>Charts</Trans>
</h3>
<div className="mt-5 grid grid-cols-2 gap-8">
<MonthlyActiveUsersChart title={_(msg`MAU (signed in)`)} data={MONTHLY_ACTIVE_USERS} />
<MonthlyActiveUsersChart
title={_(msg`Cumulative MAU (signed in)`)}
data={MONTHLY_ACTIVE_USERS}
cummulative
/>
<UserWithDocumentChart
data={MONTHLY_USERS_SIGNED}
title={_(msg`MAU (created document)`)}

View File

@ -1,8 +1,8 @@
'use client';
import { DateTime } from 'luxon';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import type { TooltipProps } from 'recharts';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import type { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent';
import type { GetUserWithDocumentMonthlyGrowth } from '@documenso/lib/server-only/admin/get-users-stats';
@ -23,7 +23,7 @@ const CustomTooltip = ({
}: TooltipProps<ValueType, NameType> & { tooltip?: string }) => {
if (active && payload && payload.length) {
return (
<div className="z-100 w-60 space-y-1 rounded-md border border-solid bg-white p-2 px-3">
<div className="z-100 w-60 space-y-1 rounded-md border border-solid bg-white p-2 px-3">
<p className="">{label}</p>
<p className="text-documenso">
{`${tooltip} : `}

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

@ -86,7 +86,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 +104,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 +157,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 +183,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 +211,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>

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

@ -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`),

View File

@ -50,7 +50,7 @@ export const RemoveTeamEmailDialog = ({ trigger, teamName, team }: RemoveTeamEma
const router = useRouter();
const { mutateAsync: deleteTeamEmail, isLoading: isDeletingTeamEmail } =
const { mutateAsync: deleteTeamEmail, isPending: isDeletingTeamEmail } =
trpc.team.deleteTeamEmail.useMutation({
onSuccess: () => {
toast({
@ -69,7 +69,7 @@ export const RemoveTeamEmailDialog = ({ trigger, teamName, team }: RemoveTeamEma
},
});
const { mutateAsync: deleteTeamEmailVerification, isLoading: isDeletingTeamEmailVerification } =
const { mutateAsync: deleteTeamEmailVerification, isPending: isDeletingTeamEmailVerification } =
trpc.team.deleteTeamEmailVerification.useMutation({
onSuccess: () => {
toast({

View File

@ -32,14 +32,14 @@ export const CurrentUserTeamsDataTable = () => {
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
const { data, isLoading, isInitialLoading, isLoadingError } = trpc.team.findTeams.useQuery(
const { data, isLoading, isLoadingError } = trpc.team.findTeams.useQuery(
{
query: parsedSearchParams.query,
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
},
{
keepPreviousData: true,
placeholderData: (previousData) => previousData,
},
);
@ -134,7 +134,7 @@ export const CurrentUserTeamsDataTable = () => {
enable: isLoadingError,
}}
skeleton={{
enable: isLoading && isInitialLoading,
enable: isLoading,
rows: 3,
component: (
<>

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