mirror of
https://github.com/documenso/documenso.git
synced 2025-11-18 18:51:37 +10:00
Compare commits
36 Commits
feat/dicta
...
wip/rr7-ne
| Author | SHA1 | Date | |
|---|---|---|---|
| 12f3b7629e | |||
| 1d7f3723bc | |||
| 4c57095ee1 | |||
| 15922d447b | |||
| 548d92c2fc | |||
| d24f67d922 | |||
| 5b395fc9ad | |||
| e128e9369e | |||
| f5bfec1990 | |||
| 82b5795636 | |||
| 4aec21a37f | |||
| 19dc43dca1 | |||
| d3392dada7 | |||
| 8373af3f41 | |||
| e5cc6455dd | |||
| b127fae0e0 | |||
| 6fa3751a72 | |||
| d164b90aa3 | |||
| 738201eb55 | |||
| 7effe66387 | |||
| 9c7910a070 | |||
| f55ccb21dd | |||
| 6b4c33a1bf | |||
| f4b2f8614e | |||
| 1057ae6d2a | |||
| 540cc5bfc1 | |||
| 381a9d3fb8 | |||
| e5a9d9ddf0 | |||
| d1913dbf9c | |||
| 8bffa7c3ed | |||
| b2af10173a | |||
| 28fb35327d | |||
| e20cb7e179 | |||
| aec44b78d0 | |||
| d7d0fca501 | |||
| f7a98180d7 |
@ -5,6 +5,7 @@ module.exports = {
|
|||||||
rules: {
|
rules: {
|
||||||
'@next/next/no-img-element': 'off',
|
'@next/next/no-img-element': 'off',
|
||||||
'no-unreachable': 'error',
|
'no-unreachable': 'error',
|
||||||
|
'react-hooks/exhaustive-deps': 'off',
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
next: {
|
next: {
|
||||||
|
|||||||
@ -111,83 +111,6 @@ The colors will be automatically converted to the appropriate format internally.
|
|||||||
|
|
||||||
4. **Consistent Radius**: Use a consistent border radius value that matches your application's design system.
|
4. **Consistent Radius**: Use a consistent border radius value that matches your application's design system.
|
||||||
|
|
||||||
## CSS Class Targets
|
|
||||||
|
|
||||||
In addition to CSS variables, specific components in the embedded experience can be targeted using CSS classes for more granular styling:
|
|
||||||
|
|
||||||
### Component Classes
|
|
||||||
|
|
||||||
| Class Name | Description |
|
|
||||||
| --------------------------------- | ----------------------------------------------------------------------- |
|
|
||||||
| `.embed--Root` | Main container for the embedded signing experience |
|
|
||||||
| `.embed--DocumentContainer` | Container for the document and signing widget |
|
|
||||||
| `.embed--DocumentViewer` | Container for the document viewer |
|
|
||||||
| `.embed--DocumentWidget` | The signing widget container |
|
|
||||||
| `.embed--DocumentWidgetContainer` | Outer container for the signing widget, handles positioning |
|
|
||||||
| `.embed--DocumentWidgetHeader` | Header section of the signing widget |
|
|
||||||
| `.embed--DocumentWidgetContent` | Main content area of the signing widget |
|
|
||||||
| `.embed--DocumentWidgetForm` | Form section within the signing widget |
|
|
||||||
| `.embed--DocumentWidgetFooter` | Footer section of the signing widget |
|
|
||||||
| `.embed--WaitingForTurn` | Container for the waiting screen when it's not the user's turn to sign |
|
|
||||||
| `.embed--DocumentCompleted` | Container for the completion screen after signing |
|
|
||||||
| `.field--FieldRootContainer` | Base container for document fields (signatures, text, checkboxes, etc.) |
|
|
||||||
|
|
||||||
Field components also expose several data attributes that can be used for styling different states:
|
|
||||||
|
|
||||||
| Data Attribute | Values | Description |
|
|
||||||
| ------------------- | ---------------------------------------------- | ------------------------------------ |
|
|
||||||
| `[data-field-type]` | `SIGNATURE`, `TEXT`, `CHECKBOX`, `RADIO`, etc. | The type of field |
|
|
||||||
| `[data-inserted]` | `true`, `false` | Whether the field has been filled |
|
|
||||||
| `[data-validate]` | `true`, `false` | Whether the field is being validated |
|
|
||||||
|
|
||||||
### Field Styling Example
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* Style all field containers */
|
|
||||||
.field--FieldRootContainer {
|
|
||||||
transition: all 200ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Style specific field types */
|
|
||||||
.field--FieldRootContainer[data-field-type='SIGNATURE'] {
|
|
||||||
background-color: rgba(0, 0, 0, 0.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Style inserted fields */
|
|
||||||
.field--FieldRootContainer[data-inserted='true'] {
|
|
||||||
background-color: var(--primary);
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Style fields being validated */
|
|
||||||
.field--FieldRootContainer[data-validate='true'] {
|
|
||||||
border-color: orange;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example Usage
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* Custom styles for the document widget */
|
|
||||||
.embed--DocumentWidget {
|
|
||||||
background-color: #ffffff;
|
|
||||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom styles for the waiting screen */
|
|
||||||
.embed--WaitingForTurn {
|
|
||||||
background-color: #f9fafb;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive adjustments for the document container */
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.embed--DocumentContainer {
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [React Integration](/developers/embedding/react)
|
- [React Integration](/developers/embedding/react)
|
||||||
|
|||||||
@ -3,8 +3,6 @@ title: Public API
|
|||||||
description: Learn how to interact with your documents programmatically using the Documenso public API.
|
description: Learn how to interact with your documents programmatically using the Documenso public API.
|
||||||
---
|
---
|
||||||
|
|
||||||
import { Callout, Steps } from 'nextra/components';
|
|
||||||
|
|
||||||
# Public API
|
# Public API
|
||||||
|
|
||||||
Documenso provides a public REST API enabling you to interact with your documents programmatically. The API exposes various HTTP endpoints that allow you to perform operations such as:
|
Documenso provides a public REST API enabling you to interact with your documents programmatically. The API exposes various HTTP endpoints that allow you to perform operations such as:
|
||||||
@ -15,24 +13,10 @@ Documenso provides a public REST API enabling you to interact with your document
|
|||||||
|
|
||||||
The documentation walks you through creating API keys and using them to authenticate your API requests. You'll also learn about the available endpoints, request and response formats, and how to use the API.
|
The documentation walks you through creating API keys and using them to authenticate your API requests. You'll also learn about the available endpoints, request and response formats, and how to use the API.
|
||||||
|
|
||||||
## API V1 - Stable
|
## Swagger Documentation
|
||||||
|
|
||||||
Check out the [API V1 documentation](https://app.documenso.com/api/v1/openapi) for details about the API endpoints, request parameters, response formats, and authentication methods.
|
The [Swagger documentation](https://app.documenso.com/api/v1/openapi) also provides information about the API endpoints, request parameters, response formats, and authentication methods.
|
||||||
|
|
||||||
## API V2 - Beta
|
|
||||||
|
|
||||||
Our new API V2 is currently in Beta. The new API features typed SDKs for TypeScript, Python and Go and example code for many more.
|
|
||||||
|
|
||||||
<Callout type="warning">
|
|
||||||
NOW IN BETA: [API V2 Documentation](https://documen.so/api-v2-docs)
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
🚀 [V2 Announcement](https://documen.so/sdk-blog)
|
|
||||||
|
|
||||||
💬 [Leave Feedback](https://documen.so/sdk-feedback)
|
|
||||||
|
|
||||||
🔔 [Breaking Changes](https://documen.so/sdk-breaking)
|
|
||||||
|
|
||||||
## Availability
|
## Availability
|
||||||
|
|
||||||
The API is available to individual users, teams and higher plans. [Fair Use](https://documen.so/fair) applies.
|
The API is available to individual users and teams.
|
||||||
|
|||||||
@ -21,7 +21,6 @@ Documenso supports Webhooks and allows you to subscribe to the following events:
|
|||||||
- `document.signed`
|
- `document.signed`
|
||||||
- `document.completed`
|
- `document.completed`
|
||||||
- `document.rejected`
|
- `document.rejected`
|
||||||
- `document.cancelled`
|
|
||||||
|
|
||||||
## Create a webhook subscription
|
## Create a webhook subscription
|
||||||
|
|
||||||
@ -38,7 +37,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:
|
To create a new webhook subscription, you need to provide the following information:
|
||||||
|
|
||||||
- Enter the webhook URL that will receive the event payload.
|
- 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`, `document.cancelled`.
|
- Select the event(s) you want to subscribe to: `document.created`, `document.sent`, `document.opened`, `document.signed`, `document.completed`, `document.rejected`.
|
||||||
- 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.
|
- 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.
|
||||||
|
|
||||||

|

|
||||||
@ -529,96 +528,6 @@ 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
|
## Availability
|
||||||
|
|
||||||
Webhooks are available to individual users and teams.
|
Webhooks are available to individual users and teams.
|
||||||
|
|||||||
28
apps/remix/.bin/build.sh
Executable file
28
apps/remix/.bin/build.sh
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Exit on error.
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
start_time=$(date +%s)
|
||||||
|
|
||||||
|
echo "[Build]: Extracting and compiling translations"
|
||||||
|
npm run translate --prefix ../../
|
||||||
|
|
||||||
|
echo "[Build]: Building app"
|
||||||
|
npm run build:app
|
||||||
|
|
||||||
|
echo "[Build]: Building server"
|
||||||
|
npm run build:server
|
||||||
|
|
||||||
|
# Copy over the entry point for the server.
|
||||||
|
cp server/main.js build/server/main.js
|
||||||
|
|
||||||
|
# Copy over all web.js translations
|
||||||
|
cp -r ../../packages/lib/translations build/server/hono/packages/lib/translations
|
||||||
|
|
||||||
|
# Time taken
|
||||||
|
end_time=$(date +%s)
|
||||||
|
|
||||||
|
echo "[Build]: Done in $((end_time - start_time)) seconds"
|
||||||
4
apps/remix/.dockerignore
Normal file
4
apps/remix/.dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.react-router
|
||||||
|
build
|
||||||
|
node_modules
|
||||||
|
README.md
|
||||||
9
apps/remix/.gitignore
vendored
Normal file
9
apps/remix/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.DS_Store
|
||||||
|
/node_modules/
|
||||||
|
|
||||||
|
# React Router
|
||||||
|
/.react-router/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.*.timestamp*
|
||||||
22
apps/remix/Dockerfile
Normal file
22
apps/remix/Dockerfile
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
FROM node:20-alpine AS development-dependencies-env
|
||||||
|
COPY . /app
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
FROM node:20-alpine AS production-dependencies-env
|
||||||
|
COPY ./package.json package-lock.json /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
|
FROM node:20-alpine AS build-env
|
||||||
|
COPY . /app/
|
||||||
|
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:20-alpine
|
||||||
|
COPY ./package.json package-lock.json /app/
|
||||||
|
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
COPY --from=build-env /app/build /app/build
|
||||||
|
WORKDIR /app
|
||||||
|
CMD ["npm", "run", "start"]
|
||||||
25
apps/remix/Dockerfile.bun
Normal file
25
apps/remix/Dockerfile.bun
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
FROM oven/bun:1 AS dependencies-env
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
FROM dependencies-env AS development-dependencies-env
|
||||||
|
COPY ./package.json bun.lockb /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN bun i --frozen-lockfile
|
||||||
|
|
||||||
|
FROM dependencies-env AS production-dependencies-env
|
||||||
|
COPY ./package.json bun.lockb /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN bun i --production
|
||||||
|
|
||||||
|
FROM dependencies-env AS build-env
|
||||||
|
COPY ./package.json bun.lockb /app/
|
||||||
|
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
WORKDIR /app
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
FROM dependencies-env
|
||||||
|
COPY ./package.json bun.lockb /app/
|
||||||
|
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
COPY --from=build-env /app/build /app/build
|
||||||
|
WORKDIR /app
|
||||||
|
CMD ["bun", "run", "start"]
|
||||||
26
apps/remix/Dockerfile.pnpm
Normal file
26
apps/remix/Dockerfile.pnpm
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
FROM node:20-alpine AS dependencies-env
|
||||||
|
RUN npm i -g pnpm
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
FROM dependencies-env AS development-dependencies-env
|
||||||
|
COPY ./package.json pnpm-lock.yaml /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN pnpm i --frozen-lockfile
|
||||||
|
|
||||||
|
FROM dependencies-env AS production-dependencies-env
|
||||||
|
COPY ./package.json pnpm-lock.yaml /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN pnpm i --prod --frozen-lockfile
|
||||||
|
|
||||||
|
FROM dependencies-env AS build-env
|
||||||
|
COPY ./package.json pnpm-lock.yaml /app/
|
||||||
|
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
WORKDIR /app
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
FROM dependencies-env
|
||||||
|
COPY ./package.json pnpm-lock.yaml /app/
|
||||||
|
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
COPY --from=build-env /app/build /app/build
|
||||||
|
WORKDIR /app
|
||||||
|
CMD ["pnpm", "start"]
|
||||||
100
apps/remix/README.md
Normal file
100
apps/remix/README.md
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# Welcome to React Router!
|
||||||
|
|
||||||
|
A modern, production-ready template for building full-stack React applications using React Router.
|
||||||
|
|
||||||
|
[](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🚀 Server-side rendering
|
||||||
|
- ⚡️ Hot Module Replacement (HMR)
|
||||||
|
- 📦 Asset bundling and optimization
|
||||||
|
- 🔄 Data loading and mutations
|
||||||
|
- 🔒 TypeScript by default
|
||||||
|
- 🎉 TailwindCSS for styling
|
||||||
|
- 📖 [React Router docs](https://reactrouter.com/)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
Install the dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
Start the development server with HMR:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Your application will be available at `http://localhost:5173`.
|
||||||
|
|
||||||
|
## Building for Production
|
||||||
|
|
||||||
|
Create a production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Docker Deployment
|
||||||
|
|
||||||
|
This template includes three Dockerfiles optimized for different package managers:
|
||||||
|
|
||||||
|
- `Dockerfile` - for npm
|
||||||
|
- `Dockerfile.pnpm` - for pnpm
|
||||||
|
- `Dockerfile.bun` - for bun
|
||||||
|
|
||||||
|
To build and run using Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For npm
|
||||||
|
docker build -t my-app .
|
||||||
|
|
||||||
|
# For pnpm
|
||||||
|
docker build -f Dockerfile.pnpm -t my-app .
|
||||||
|
|
||||||
|
# For bun
|
||||||
|
docker build -f Dockerfile.bun -t my-app .
|
||||||
|
|
||||||
|
# Run the container
|
||||||
|
docker run -p 3000:3000 my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
The containerized application can be deployed to any platform that supports Docker, including:
|
||||||
|
|
||||||
|
- AWS ECS
|
||||||
|
- Google Cloud Run
|
||||||
|
- Azure Container Apps
|
||||||
|
- Digital Ocean App Platform
|
||||||
|
- Fly.io
|
||||||
|
- Railway
|
||||||
|
|
||||||
|
### DIY Deployment
|
||||||
|
|
||||||
|
If you're familiar with deploying Node applications, the built-in app server is production-ready.
|
||||||
|
|
||||||
|
Make sure to deploy the output of `npm run build`
|
||||||
|
|
||||||
|
```
|
||||||
|
├── package.json
|
||||||
|
├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
|
||||||
|
├── build/
|
||||||
|
│ ├── client/ # Static assets
|
||||||
|
│ └── server/ # Server-side code
|
||||||
|
```
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Built with ❤️ using React Router.
|
||||||
24
apps/remix/app/app.css
Normal file
24
apps/remix/app/app.css
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
@import '@documenso/ui/styles/theme.css';
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src: url('/public/fonts/inter-regular.ttf') format('ttf');
|
||||||
|
/* font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap; */
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Caveat';
|
||||||
|
src: url('/public/fonts/caveat.ttf') format('ttf');
|
||||||
|
/* font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap; */
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--font-sans: 'Inter';
|
||||||
|
--font-signature: 'Caveat';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +1,11 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { signOut } from 'next-auth/react';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
|
||||||
import type { User } from '@documenso/prisma/client';
|
import { authClient } from '@documenso/auth/client';
|
||||||
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -23,12 +22,13 @@ import { Input } from '@documenso/ui/primitives/input';
|
|||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DeleteAccountDialogProps = {
|
export type AccountDeleteDialogProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
user: User;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProps) => {
|
export const AccountDeleteDialog = ({ className }: AccountDeleteDialogProps) => {
|
||||||
|
const { user } = useSession();
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProp
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await signOut({ callbackUrl: '/' });
|
return await authClient.signOut();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
@ -118,7 +118,7 @@ export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProp
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{!hasTwoFactorAuthentication && (
|
{!hasTwoFactorAuthentication && (
|
||||||
<div className="mt-4">
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
<Trans>
|
<Trans>
|
||||||
Please type{' '}
|
Please type{' '}
|
||||||
@ -1,13 +1,11 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Document } from '@prisma/client';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
import type { Document } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -23,15 +21,15 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type SuperDeleteDocumentDialogProps = {
|
export type AdminDocumentDeleteDialogProps = {
|
||||||
document: Document;
|
document: Document;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialogProps) => {
|
export const AdminDocumentDeleteDialog = ({ document }: AdminDocumentDeleteDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [reason, setReason] = useState('');
|
const [reason, setReason] = useState('');
|
||||||
|
|
||||||
@ -52,7 +50,7 @@ export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialo
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push('/admin/documents');
|
await navigate('/admin/documents');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
@ -1,15 +1,13 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { User } from '@prisma/client';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { User } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -25,17 +23,15 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DeleteUserDialogProps = {
|
export type AdminUserDeleteDialogProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
user: User;
|
user: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteUserDialog = ({ className, user }: DeleteUserDialogProps) => {
|
export const AdminUserDeleteDialog = ({ className, user }: AdminUserDeleteDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const navigate = useNavigate();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
|
||||||
const { mutateAsync: deleteUser, isPending: isDeletingUser } =
|
const { mutateAsync: deleteUser, isPending: isDeletingUser } =
|
||||||
@ -47,13 +43,13 @@ export const DeleteUserDialog = ({ className, user }: DeleteUserDialogProps) =>
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await navigate('/admin/users');
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Account deleted`),
|
title: _(msg`Account deleted`),
|
||||||
description: _(msg`The account has been deleted successfully.`),
|
description: _(msg`The account has been deleted successfully.`),
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push('/admin/users');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -1,13 +1,12 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { User } from '@prisma/client';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { User } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -23,12 +22,15 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DisableUserDialogProps = {
|
export type AdminUserDisableDialogProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
userToDisable: User;
|
userToDisable: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DisableUserDialog = ({ className, userToDisable }: DisableUserDialogProps) => {
|
export const AdminUserDisableDialog = ({
|
||||||
|
className,
|
||||||
|
userToDisable,
|
||||||
|
}: AdminUserDisableDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@ -1,13 +1,12 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { User } from '@prisma/client';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { User } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -23,12 +22,12 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type EnableUserDialogProps = {
|
export type AdminUserEnableDialogProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
userToEnable: User;
|
userToEnable: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EnableUserDialog = ({ className, userToEnable }: EnableUserDialogProps) => {
|
export const AdminUserEnableDialog = ({ className, userToEnable }: AdminUserEnableDialogProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
@ -1,13 +1,12 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { DocumentStatus } from '@prisma/client';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -22,26 +21,26 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
type DeleteDocumentDialogProps = {
|
type DocumentDeleteDialogProps = {
|
||||||
id: number;
|
id: number;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
|
onDelete?: () => Promise<void> | void;
|
||||||
status: DocumentStatus;
|
status: DocumentStatus;
|
||||||
documentTitle: string;
|
documentTitle: string;
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
canManageDocument: boolean;
|
canManageDocument: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteDocumentDialog = ({
|
export const DocumentDeleteDialog = ({
|
||||||
id,
|
id,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
|
onDelete,
|
||||||
status,
|
status,
|
||||||
documentTitle,
|
documentTitle,
|
||||||
canManageDocument,
|
canManageDocument,
|
||||||
}: DeleteDocumentDialogProps) => {
|
}: DocumentDeleteDialogProps) => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { refreshLimits } = useLimits();
|
const { refreshLimits } = useLimits();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -52,8 +51,7 @@ export const DeleteDocumentDialog = ({
|
|||||||
const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
|
const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
|
||||||
|
|
||||||
const { mutateAsync: deleteDocument, isPending } = trpcReact.document.deleteDocument.useMutation({
|
const { mutateAsync: deleteDocument, isPending } = trpcReact.document.deleteDocument.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: async () => {
|
||||||
router.refresh();
|
|
||||||
void refreshLimits();
|
void refreshLimits();
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@ -62,8 +60,18 @@ export const DeleteDocumentDialog = ({
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await onDelete?.();
|
||||||
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
},
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast({
|
||||||
|
title: _(msg`Something went wrong`),
|
||||||
|
description: _(msg`This document could not be deleted at this time. Please try again.`),
|
||||||
|
variant: 'destructive',
|
||||||
|
duration: 7500,
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -73,19 +81,6 @@ export const DeleteDocumentDialog = ({
|
|||||||
}
|
}
|
||||||
}, [open, status]);
|
}, [open, status]);
|
||||||
|
|
||||||
const onDelete = async () => {
|
|
||||||
try {
|
|
||||||
await deleteDocument({ documentId: id });
|
|
||||||
} catch {
|
|
||||||
toast({
|
|
||||||
title: _(msg`Something went wrong`),
|
|
||||||
description: _(msg`This document could not be deleted at this time. Please try again.`),
|
|
||||||
variant: 'destructive',
|
|
||||||
duration: 7500,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setInputValue(event.target.value);
|
setInputValue(event.target.value);
|
||||||
setIsDeleteEnabled(event.target.value === _(deleteMessage));
|
setIsDeleteEnabled(event.target.value === _(deleteMessage));
|
||||||
@ -194,7 +189,7 @@ export const DeleteDocumentDialog = ({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
loading={isPending}
|
loading={isPending}
|
||||||
onClick={onDelete}
|
onClick={() => void deleteDocument({ documentId: id })}
|
||||||
disabled={!isDeleteEnabled && canManageDocument}
|
disabled={!isDeleteEnabled && canManageDocument}
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
>
|
>
|
||||||
@ -1,10 +1,9 @@
|
|||||||
import { useRouter } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import type { Team } from '@documenso/prisma/client';
|
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -17,27 +16,34 @@ import {
|
|||||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
type DuplicateDocumentDialogProps = {
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
|
type DocumentDuplicateDialogProps = {
|
||||||
id: number;
|
id: number;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
team?: Pick<Team, 'id' | 'url'>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DuplicateDocumentDialog = ({
|
export const DocumentDuplicateDialog = ({
|
||||||
id,
|
id,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
team,
|
}: DocumentDuplicateDialogProps) => {
|
||||||
}: DuplicateDocumentDialogProps) => {
|
const navigate = useNavigate();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({
|
const team = useOptionalCurrentTeam();
|
||||||
|
|
||||||
|
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery(
|
||||||
|
{
|
||||||
documentId: id,
|
documentId: id,
|
||||||
});
|
},
|
||||||
|
{
|
||||||
|
enabled: open === true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const documentData = document?.documentData
|
const documentData = document?.documentData
|
||||||
? {
|
? {
|
||||||
@ -50,15 +56,14 @@ export const DuplicateDocumentDialog = ({
|
|||||||
|
|
||||||
const { mutateAsync: duplicateDocument, isPending: isDuplicateLoading } =
|
const { mutateAsync: duplicateDocument, isPending: isDuplicateLoading } =
|
||||||
trpcReact.document.duplicateDocument.useMutation({
|
trpcReact.document.duplicateDocument.useMutation({
|
||||||
onSuccess: ({ documentId }) => {
|
onSuccess: async ({ documentId }) => {
|
||||||
router.push(`${documentsPath}/${documentId}/edit`);
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Document Duplicated`),
|
title: _(msg`Document Duplicated`),
|
||||||
description: _(msg`Your document has been successfully duplicated.`),
|
description: _(msg`Your document has been successfully duplicated.`),
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await navigate(`${documentsPath}/${documentId}/edit`);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -1,11 +1,10 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -26,30 +25,28 @@ import {
|
|||||||
} from '@documenso/ui/primitives/select';
|
} from '@documenso/ui/primitives/select';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
type MoveDocumentDialogProps = {
|
type DocumentMoveDialogProps = {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocumentDialogProps) => {
|
export const DocumentMoveDialog = ({ documentId, open, onOpenChange }: DocumentMoveDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
|
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
|
||||||
|
|
||||||
const { data: teams, isPending: isLoadingTeams } = trpc.team.getTeams.useQuery();
|
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
|
||||||
|
|
||||||
const { mutateAsync: moveDocument, isPending } = trpc.document.moveDocumentToTeam.useMutation({
|
const { mutateAsync: moveDocument, isPending } = trpc.document.moveDocumentToTeam.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
router.refresh();
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Document moved`),
|
title: _(msg`Document moved`),
|
||||||
description: _(msg`The document has been successfully moved to the selected team.`),
|
description: _(msg`The document has been successfully moved to the selected team.`),
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@ -97,9 +94,7 @@ export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocum
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Avatar className="h-8 w-8">
|
<Avatar className="h-8 w-8">
|
||||||
{team.avatarImageId && (
|
{team.avatarImageId && (
|
||||||
<AvatarImage
|
<AvatarImage src={formatAvatarUrl(team.avatarImageId)} />
|
||||||
src={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${team.avatarImageId}`}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<AvatarFallback className="text-sm text-gray-400">
|
<AvatarFallback className="text-sm text-gray-400">
|
||||||
@ -1,19 +1,18 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Team } from '@prisma/client';
|
||||||
|
import { type Document, type Recipient, SigningStatus } from '@prisma/client';
|
||||||
import { History } from 'lucide-react';
|
import { History } from 'lucide-react';
|
||||||
import { useSession } from 'next-auth/react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
||||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { Team } from '@documenso/prisma/client';
|
|
||||||
import { type Document, type Recipient, SigningStatus } from '@documenso/prisma/client';
|
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -37,16 +36,17 @@ import {
|
|||||||
} from '@documenso/ui/primitives/form/form';
|
} from '@documenso/ui/primitives/form/form';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { StackAvatar } from '~/components/(dashboard)/avatar/stack-avatar';
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
|
import { StackAvatar } from '../general/stack-avatar';
|
||||||
|
|
||||||
const FORM_ID = 'resend-email';
|
const FORM_ID = 'resend-email';
|
||||||
|
|
||||||
export type ResendDocumentActionItemProps = {
|
export type DocumentResendDialogProps = {
|
||||||
document: Document & {
|
document: Document & {
|
||||||
team: Pick<Team, 'id' | 'url'> | null;
|
team: Pick<Team, 'id' | 'url'> | null;
|
||||||
};
|
};
|
||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
team?: Pick<Team, 'id' | 'url'>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZResendDocumentFormSchema = z.object({
|
export const ZResendDocumentFormSchema = z.object({
|
||||||
@ -57,17 +57,15 @@ export const ZResendDocumentFormSchema = z.object({
|
|||||||
|
|
||||||
export type TResendDocumentFormSchema = z.infer<typeof ZResendDocumentFormSchema>;
|
export type TResendDocumentFormSchema = z.infer<typeof ZResendDocumentFormSchema>;
|
||||||
|
|
||||||
export const ResendDocumentActionItem = ({
|
export const DocumentResendDialog = ({ document, recipients }: DocumentResendDialogProps) => {
|
||||||
document,
|
const { user } = useSession();
|
||||||
recipients,
|
const team = useOptionalCurrentTeam();
|
||||||
team,
|
|
||||||
}: ResendDocumentActionItemProps) => {
|
|
||||||
const { data: session } = useSession();
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const isOwner = document.userId === session?.user?.id;
|
const isOwner = document.userId === user.id;
|
||||||
const isCurrentTeamDocument = team && document.team?.url === team.url;
|
const isCurrentTeamDocument = team && document.team?.url === team.url;
|
||||||
|
|
||||||
const isDisabled =
|
const isDisabled =
|
||||||
@ -1,10 +1,9 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { startRegistration } from '@simplewebauthn/browser';
|
import { startRegistration } from '@simplewebauthn/browser';
|
||||||
import { KeyRoundIcon } from 'lucide-react';
|
import { KeyRoundIcon } from 'lucide-react';
|
||||||
@ -38,7 +37,7 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type CreatePasskeyDialogProps = {
|
export type PasskeyCreateDialogProps = {
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
@ -51,7 +50,7 @@ type TCreatePasskeyFormSchema = z.infer<typeof ZCreatePasskeyFormSchema>;
|
|||||||
|
|
||||||
const parser = new UAParser();
|
const parser = new UAParser();
|
||||||
|
|
||||||
export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePasskeyDialogProps) => {
|
export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCreateDialogProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [formError, setFormError] = useState<string | null>(null);
|
const [formError, setFormError] = useState<string | null>(null);
|
||||||
|
|
||||||
@ -1,18 +1,17 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Plural, Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Plural, Trans } from '@lingui/react/macro';
|
||||||
|
import type { Template, TemplateDirectLink } from '@prisma/client';
|
||||||
|
import { TemplateType } from '@prisma/client';
|
||||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { CheckCircle2Icon, CircleIcon } from 'lucide-react';
|
import { CheckCircle2Icon, CircleIcon } from 'lucide-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { P, match } from 'ts-pattern';
|
import { P, match } from 'ts-pattern';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { Template, TemplateDirectLink } from '@documenso/prisma/client';
|
|
||||||
import { TemplateType } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import {
|
import {
|
||||||
MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH,
|
MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH,
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { Loader, TagIcon } from 'lucide-react';
|
import { Loader, TagIcon } from 'lucide-react';
|
||||||
@ -20,18 +21,18 @@ import {
|
|||||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type CreateTeamCheckoutDialogProps = {
|
export type TeamCheckoutCreateDialogProps = {
|
||||||
pendingTeamId: number | null;
|
pendingTeamId: number | null;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
|
|
||||||
const MotionCard = motion(Card);
|
const MotionCard = motion(Card);
|
||||||
|
|
||||||
export const CreateTeamCheckoutDialog = ({
|
export const TeamCheckoutCreateDialog = ({
|
||||||
pendingTeamId,
|
pendingTeamId,
|
||||||
onClose,
|
onClose,
|
||||||
...props
|
...props
|
||||||
}: CreateTeamCheckoutDialogProps) => {
|
}: TeamCheckoutCreateDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@ -1,18 +1,17 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useSearchParams } from 'react-router';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { ZCreateTeamMutationSchema } from '@documenso/trpc/server/team-router/schema';
|
import { ZCreateTeamMutationSchema } from '@documenso/trpc/server/team-router/schema';
|
||||||
@ -37,7 +36,7 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type CreateTeamDialogProps = {
|
export type TeamCreateDialogProps = {
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
|
|
||||||
@ -48,12 +47,12 @@ const ZCreateTeamFormSchema = ZCreateTeamMutationSchema.pick({
|
|||||||
|
|
||||||
type TCreateTeamFormSchema = z.infer<typeof ZCreateTeamFormSchema>;
|
type TCreateTeamFormSchema = z.infer<typeof ZCreateTeamFormSchema>;
|
||||||
|
|
||||||
export const CreateTeamDialog = ({ trigger, ...props }: CreateTeamDialogProps) => {
|
export const TeamCreateDialog = ({ trigger, ...props }: TeamCreateDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const searchParams = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const updateSearchParams = useUpdateSearchParams();
|
const updateSearchParams = useUpdateSearchParams();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@ -80,7 +79,7 @@ export const CreateTeamDialog = ({ trigger, ...props }: CreateTeamDialogProps) =
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|
||||||
if (response.paymentRequired) {
|
if (response.paymentRequired) {
|
||||||
router.push(`/settings/teams?tab=pending&checkout=${response.pendingTeamId}`);
|
await navigate(`/settings/teams?tab=pending&checkout=${response.pendingTeamId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,7 +200,7 @@ export const CreateTeamDialog = ({ trigger, ...props }: CreateTeamDialogProps) =
|
|||||||
{!form.formState.errors.teamUrl && (
|
{!form.formState.errors.teamUrl && (
|
||||||
<span className="text-foreground/50 text-xs font-normal">
|
<span className="text-foreground/50 text-xs font-normal">
|
||||||
{field.value ? (
|
{field.value ? (
|
||||||
`${WEBAPP_BASE_URL}/t/${field.value}`
|
`${NEXT_PUBLIC_WEBAPP_URL()}/t/${field.value}`
|
||||||
) : (
|
) : (
|
||||||
<Trans>A unique URL to identify your team</Trans>
|
<Trans>A unique URL to identify your team</Trans>
|
||||||
)}
|
)}
|
||||||
@ -1,13 +1,11 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
@ -34,14 +32,14 @@ import { Input } from '@documenso/ui/primitives/input';
|
|||||||
import type { Toast } from '@documenso/ui/primitives/use-toast';
|
import type { Toast } from '@documenso/ui/primitives/use-toast';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DeleteTeamDialogProps = {
|
export type TeamDeleteDialogProps = {
|
||||||
teamId: number;
|
teamId: number;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteTeamDialog = ({ trigger, teamId, teamName }: DeleteTeamDialogProps) => {
|
export const TeamDeleteDialog = ({ trigger, teamId, teamName }: TeamDeleteDialogProps) => {
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -74,9 +72,9 @@ export const DeleteTeamDialog = ({ trigger, teamId, teamName }: DeleteTeamDialog
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
setOpen(false);
|
await navigate('/settings/teams');
|
||||||
|
|
||||||
router.push('/settings/teams');
|
setOpen(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -1,15 +1,13 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
@ -36,7 +34,7 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type AddTeamEmailDialogProps = {
|
export type TeamEmailAddDialogProps = {
|
||||||
teamId: number;
|
teamId: number;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
@ -48,13 +46,12 @@ const ZCreateTeamEmailFormSchema = ZCreateTeamEmailVerificationMutationSchema.pi
|
|||||||
|
|
||||||
type TCreateTeamEmailFormSchema = z.infer<typeof ZCreateTeamEmailFormSchema>;
|
type TCreateTeamEmailFormSchema = z.infer<typeof ZCreateTeamEmailFormSchema>;
|
||||||
|
|
||||||
export const AddTeamEmailDialog = ({ teamId, trigger, ...props }: AddTeamEmailDialogProps) => {
|
export const TeamEmailAddDialog = ({ teamId, trigger, ...props }: TeamEmailAddDialogProps) => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const form = useForm<TCreateTeamEmailFormSchema>({
|
const form = useForm<TCreateTeamEmailFormSchema>({
|
||||||
resolver: zodResolver(ZCreateTeamEmailFormSchema),
|
resolver: zodResolver(ZCreateTeamEmailFormSchema),
|
||||||
@ -81,7 +78,7 @@ export const AddTeamEmailDialog = ({ teamId, trigger, ...props }: AddTeamEmailDi
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
await revalidate();
|
||||||
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -1,15 +1,13 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Prisma } from '@prisma/client';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { Prisma } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert } from '@documenso/ui/primitives/alert';
|
import { Alert } from '@documenso/ui/primitives/alert';
|
||||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||||
@ -25,7 +23,7 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type RemoveTeamEmailDialogProps = {
|
export type TeamEmailDeleteDialogProps = {
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
team: Prisma.TeamGetPayload<{
|
team: Prisma.TeamGetPayload<{
|
||||||
@ -42,13 +40,12 @@ export type RemoveTeamEmailDialogProps = {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RemoveTeamEmailDialog = ({ trigger, teamName, team }: RemoveTeamEmailDialogProps) => {
|
export const TeamEmailDeleteDialog = ({ trigger, teamName, team }: TeamEmailDeleteDialogProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { mutateAsync: deleteTeamEmail, isPending: isDeletingTeamEmail } =
|
const { mutateAsync: deleteTeamEmail, isPending: isDeletingTeamEmail } =
|
||||||
trpc.team.deleteTeamEmail.useMutation({
|
trpc.team.deleteTeamEmail.useMutation({
|
||||||
@ -97,7 +94,7 @@ export const RemoveTeamEmailDialog = ({ trigger, teamName, team }: RemoveTeamEma
|
|||||||
await deleteTeamEmailVerification({ teamId: team.id });
|
await deleteTeamEmailVerification({ teamId: team.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
router.refresh();
|
await revalidate();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -127,7 +124,7 @@ export const RemoveTeamEmailDialog = ({ trigger, teamName, team }: RemoveTeamEma
|
|||||||
<Alert variant="neutral" padding="tight">
|
<Alert variant="neutral" padding="tight">
|
||||||
<AvatarWithText
|
<AvatarWithText
|
||||||
avatarClass="h-12 w-12"
|
avatarClass="h-12 w-12"
|
||||||
avatarSrc={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${team.avatarImageId}`}
|
avatarSrc={formatAvatarUrl(team.avatarImageId)}
|
||||||
avatarFallback={extractInitials(
|
avatarFallback={extractInitials(
|
||||||
(team.teamEmail?.name || team.emailVerification?.name) ?? '',
|
(team.teamEmail?.name || team.emailVerification?.name) ?? '',
|
||||||
)}
|
)}
|
||||||
@ -1,17 +1,15 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { TeamEmail } from '@prisma/client';
|
||||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { TeamEmail } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -34,7 +32,7 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type UpdateTeamEmailDialogProps = {
|
export type TeamEmailUpdateDialogProps = {
|
||||||
teamEmail: TeamEmail;
|
teamEmail: TeamEmail;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
@ -45,17 +43,16 @@ const ZUpdateTeamEmailFormSchema = z.object({
|
|||||||
|
|
||||||
type TUpdateTeamEmailFormSchema = z.infer<typeof ZUpdateTeamEmailFormSchema>;
|
type TUpdateTeamEmailFormSchema = z.infer<typeof ZUpdateTeamEmailFormSchema>;
|
||||||
|
|
||||||
export const UpdateTeamEmailDialog = ({
|
export const TeamEmailUpdateDialog = ({
|
||||||
teamEmail,
|
teamEmail,
|
||||||
trigger,
|
trigger,
|
||||||
...props
|
...props
|
||||||
}: UpdateTeamEmailDialogProps) => {
|
}: TeamEmailUpdateDialogProps) => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const form = useForm<TUpdateTeamEmailFormSchema>({
|
const form = useForm<TUpdateTeamEmailFormSchema>({
|
||||||
resolver: zodResolver(ZUpdateTeamEmailFormSchema),
|
resolver: zodResolver(ZUpdateTeamEmailFormSchema),
|
||||||
@ -81,7 +78,7 @@ export const UpdateTeamEmailDialog = ({
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
await revalidate();
|
||||||
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -1,13 +1,12 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { TeamMemberRole } from '@prisma/client';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
|
||||||
import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
|
import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
|
||||||
import type { TeamMemberRole } from '@documenso/prisma/client';
|
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert } from '@documenso/ui/primitives/alert';
|
import { Alert } from '@documenso/ui/primitives/alert';
|
||||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||||
@ -23,7 +22,7 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type LeaveTeamDialogProps = {
|
export type TeamLeaveDialogProps = {
|
||||||
teamId: number;
|
teamId: number;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
teamAvatarImageId?: string | null;
|
teamAvatarImageId?: string | null;
|
||||||
@ -31,13 +30,13 @@ export type LeaveTeamDialogProps = {
|
|||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LeaveTeamDialog = ({
|
export const TeamLeaveDialog = ({
|
||||||
trigger,
|
trigger,
|
||||||
teamId,
|
teamId,
|
||||||
teamName,
|
teamName,
|
||||||
teamAvatarImageId,
|
teamAvatarImageId,
|
||||||
role,
|
role,
|
||||||
}: LeaveTeamDialogProps) => {
|
}: TeamLeaveDialogProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -89,7 +88,7 @@ export const LeaveTeamDialog = ({
|
|||||||
<Alert variant="neutral" padding="tight">
|
<Alert variant="neutral" padding="tight">
|
||||||
<AvatarWithText
|
<AvatarWithText
|
||||||
avatarClass="h-12 w-12"
|
avatarClass="h-12 w-12"
|
||||||
avatarSrc={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${teamAvatarImageId}`}
|
avatarSrc={formatAvatarUrl(teamAvatarImageId)}
|
||||||
avatarFallback={teamName.slice(0, 1).toUpperCase()}
|
avatarFallback={teamName.slice(0, 1).toUpperCase()}
|
||||||
primaryText={teamName}
|
primaryText={teamName}
|
||||||
secondaryText={_(TEAM_MEMBER_ROLE_MAP[role])}
|
secondaryText={_(TEAM_MEMBER_ROLE_MAP[role])}
|
||||||
@ -1,9 +1,8 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert } from '@documenso/ui/primitives/alert';
|
import { Alert } from '@documenso/ui/primitives/alert';
|
||||||
@ -20,7 +19,7 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DeleteTeamMemberDialogProps = {
|
export type TeamMemberDeleteDialogProps = {
|
||||||
teamId: number;
|
teamId: number;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
teamMemberId: number;
|
teamMemberId: number;
|
||||||
@ -29,14 +28,14 @@ export type DeleteTeamMemberDialogProps = {
|
|||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteTeamMemberDialog = ({
|
export const TeamMemberDeleteDialog = ({
|
||||||
trigger,
|
trigger,
|
||||||
teamId,
|
teamId,
|
||||||
teamName,
|
teamName,
|
||||||
teamMemberId,
|
teamMemberId,
|
||||||
teamMemberName,
|
teamMemberName,
|
||||||
teamMemberEmail,
|
teamMemberEmail,
|
||||||
}: DeleteTeamMemberDialogProps) => {
|
}: TeamMemberDeleteDialogProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -1,10 +1,10 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { TeamMemberRole } from '@prisma/client';
|
||||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { Download, Mail, MailIcon, PlusCircle, Trash, Upload, UsersIcon } from 'lucide-react';
|
import { Download, Mail, MailIcon, PlusCircle, Trash, Upload, UsersIcon } from 'lucide-react';
|
||||||
import Papa, { type ParseResult } from 'papaparse';
|
import Papa, { type ParseResult } from 'papaparse';
|
||||||
@ -13,7 +13,6 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
||||||
import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
|
import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
|
||||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { ZCreateTeamMemberInvitesMutationSchema } from '@documenso/trpc/server/team-router/schema';
|
import { ZCreateTeamMemberInvitesMutationSchema } from '@documenso/trpc/server/team-router/schema';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@ -47,9 +46,9 @@ import {
|
|||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type InviteTeamMembersDialogProps = {
|
import { useCurrentTeam } from '~/providers/team';
|
||||||
currentUserTeamRole: TeamMemberRole;
|
|
||||||
teamId: number;
|
export type TeamMemberInviteDialogProps = {
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
|
|
||||||
@ -96,12 +95,7 @@ const ZImportTeamMemberSchema = z.array(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const InviteTeamMembersDialog = ({
|
export const TeamMemberInviteDialog = ({ trigger, ...props }: TeamMemberInviteDialogProps) => {
|
||||||
currentUserTeamRole,
|
|
||||||
teamId,
|
|
||||||
trigger,
|
|
||||||
...props
|
|
||||||
}: InviteTeamMembersDialogProps) => {
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [invitationType, setInvitationType] = useState<TabTypes>('INDIVIDUAL');
|
const [invitationType, setInvitationType] = useState<TabTypes>('INDIVIDUAL');
|
||||||
@ -109,6 +103,8 @@ export const InviteTeamMembersDialog = ({
|
|||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const team = useCurrentTeam();
|
||||||
|
|
||||||
const form = useForm<TInviteTeamMembersFormSchema>({
|
const form = useForm<TInviteTeamMembersFormSchema>({
|
||||||
resolver: zodResolver(ZInviteTeamMembersFormSchema),
|
resolver: zodResolver(ZInviteTeamMembersFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -142,7 +138,7 @@ export const InviteTeamMembersDialog = ({
|
|||||||
const onFormSubmit = async ({ invitations }: TInviteTeamMembersFormSchema) => {
|
const onFormSubmit = async ({ invitations }: TInviteTeamMembersFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await createTeamMemberInvites({
|
await createTeamMemberInvites({
|
||||||
teamId,
|
teamId: team.id,
|
||||||
invitations,
|
invitations,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -204,7 +200,7 @@ export const InviteTeamMembersDialog = ({
|
|||||||
|
|
||||||
setInvitationType('INDIVIDUAL');
|
setInvitationType('INDIVIDUAL');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err.message);
|
console.error(err);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Something went wrong`),
|
title: _(msg`Something went wrong`),
|
||||||
@ -325,11 +321,13 @@ export const InviteTeamMembersDialog = ({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
||||||
<SelectContent position="popper">
|
<SelectContent position="popper">
|
||||||
{TEAM_MEMBER_ROLE_HIERARCHY[currentUserTeamRole].map((role) => (
|
{TEAM_MEMBER_ROLE_HIERARCHY[team.currentTeamMember.role].map(
|
||||||
|
(role) => (
|
||||||
<SelectItem key={role} value={role}>
|
<SelectItem key={role} value={role}>
|
||||||
{_(TEAM_MEMBER_ROLE_MAP[role]) ?? role}
|
{_(TEAM_MEMBER_ROLE_MAP[role]) ?? role}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
),
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -1,17 +1,16 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { TeamMemberRole } from '@prisma/client';
|
||||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
|
import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
|
||||||
import { isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
|
import { isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
|
||||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -40,7 +39,7 @@ import {
|
|||||||
} from '@documenso/ui/primitives/select';
|
} from '@documenso/ui/primitives/select';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type UpdateTeamMemberDialogProps = {
|
export type TeamMemberUpdateDialogProps = {
|
||||||
currentUserTeamRole: TeamMemberRole;
|
currentUserTeamRole: TeamMemberRole;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
teamId: number;
|
teamId: number;
|
||||||
@ -55,7 +54,7 @@ const ZUpdateTeamMemberFormSchema = z.object({
|
|||||||
|
|
||||||
type ZUpdateTeamMemberSchema = z.infer<typeof ZUpdateTeamMemberFormSchema>;
|
type ZUpdateTeamMemberSchema = z.infer<typeof ZUpdateTeamMemberFormSchema>;
|
||||||
|
|
||||||
export const UpdateTeamMemberDialog = ({
|
export const TeamMemberUpdateDialog = ({
|
||||||
currentUserTeamRole,
|
currentUserTeamRole,
|
||||||
trigger,
|
trigger,
|
||||||
teamId,
|
teamId,
|
||||||
@ -63,7 +62,7 @@ export const UpdateTeamMemberDialog = ({
|
|||||||
teamMemberName,
|
teamMemberName,
|
||||||
teamMemberRole,
|
teamMemberRole,
|
||||||
...props
|
...props
|
||||||
}: UpdateTeamMemberDialogProps) => {
|
}: TeamMemberUpdateDialogProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -1,14 +1,12 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||||
@ -42,24 +40,24 @@ import {
|
|||||||
} from '@documenso/ui/primitives/select';
|
} from '@documenso/ui/primitives/select';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type TransferTeamDialogProps = {
|
export type TeamTransferDialogProps = {
|
||||||
teamId: number;
|
teamId: number;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
ownerUserId: number;
|
ownerUserId: number;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TransferTeamDialog = ({
|
export const TeamTransferDialog = ({
|
||||||
trigger,
|
trigger,
|
||||||
teamId,
|
teamId,
|
||||||
teamName,
|
teamName,
|
||||||
ownerUserId,
|
ownerUserId,
|
||||||
}: TransferTeamDialogProps) => {
|
}: TeamTransferDialogProps) => {
|
||||||
const router = useRouter();
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const { mutateAsync: requestTeamOwnershipTransfer } =
|
const { mutateAsync: requestTeamOwnershipTransfer } =
|
||||||
trpc.team.requestTeamOwnershipTransfer.useMutation();
|
trpc.team.requestTeamOwnershipTransfer.useMutation();
|
||||||
@ -67,7 +65,7 @@ export const TransferTeamDialog = ({
|
|||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
refetch: refetchTeamMembers,
|
refetch: refetchTeamMembers,
|
||||||
isPending: loadingTeamMembers,
|
isLoading: loadingTeamMembers,
|
||||||
isLoadingError: loadingTeamMembersError,
|
isLoadingError: loadingTeamMembersError,
|
||||||
} = trpc.team.getTeamMembers.useQuery({
|
} = trpc.team.getTeamMembers.useQuery({
|
||||||
teamId,
|
teamId,
|
||||||
@ -102,7 +100,7 @@ export const TransferTeamDialog = ({
|
|||||||
clearPaymentMethods,
|
clearPaymentMethods,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
await revalidate();
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Success`),
|
title: _(msg`Success`),
|
||||||
@ -1,15 +1,12 @@
|
|||||||
'use client';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { FilePlus, Loader } from 'lucide-react';
|
import { FilePlus, Loader } from 'lucide-react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -26,21 +23,21 @@ import {
|
|||||||
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
|
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
type NewTemplateDialogProps = {
|
type TemplateCreateDialogProps = {
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
templateRootPath: string;
|
templateRootPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps) => {
|
export const TemplateCreateDialog = ({ templateRootPath }: TemplateCreateDialogProps) => {
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { data: session } = useSession();
|
const { user } = useSession();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
|
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
|
||||||
|
|
||||||
const [showNewTemplateDialog, setShowNewTemplateDialog] = useState(false);
|
const [showTemplateCreateDialog, setShowTemplateCreateDialog] = useState(false);
|
||||||
const [isUploadingFile, setIsUploadingFile] = useState(false);
|
const [isUploadingFile, setIsUploadingFile] = useState(false);
|
||||||
|
|
||||||
const onFileDrop = async (file: File) => {
|
const onFileDrop = async (file: File) => {
|
||||||
@ -51,15 +48,11 @@ export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps)
|
|||||||
setIsUploadingFile(true);
|
setIsUploadingFile(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { type, data } = await putPdfFile(file);
|
const response = await putPdfFile(file);
|
||||||
const { id: templateDocumentDataId } = await createDocumentData({
|
|
||||||
type,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { id } = await createTemplate({
|
const { id } = await createTemplate({
|
||||||
title: file.name,
|
title: file.name,
|
||||||
templateDocumentDataId,
|
templateDocumentDataId: response.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@ -70,9 +63,9 @@ export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps)
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
setShowNewTemplateDialog(false);
|
setShowTemplateCreateDialog(false);
|
||||||
|
|
||||||
router.push(`${templateRootPath}/${id}/edit`);
|
await navigate(`${templateRootPath}/${id}/edit`);
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Something went wrong`),
|
title: _(msg`Something went wrong`),
|
||||||
@ -86,11 +79,12 @@ export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps)
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={showNewTemplateDialog}
|
open={showTemplateCreateDialog}
|
||||||
onOpenChange={(value) => !isUploadingFile && setShowNewTemplateDialog(value)}
|
onOpenChange={(value) => !isUploadingFile && setShowTemplateCreateDialog(value)}
|
||||||
>
|
>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className="cursor-pointer" disabled={!session?.user.emailVerified}>
|
{/* Todo: Wouldn't this break for google? */}
|
||||||
|
<Button className="cursor-pointer" disabled={!user.emailVerified}>
|
||||||
<FilePlus className="-ml-1 mr-2 h-4 w-4" />
|
<FilePlus className="-ml-1 mr-2 h-4 w-4" />
|
||||||
<Trans>New Template</Trans>
|
<Trans>New Template</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import { useRouter } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -15,22 +14,25 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
type DeleteTemplateDialogProps = {
|
type TemplateDeleteDialogProps = {
|
||||||
id: number;
|
id: number;
|
||||||
teamId?: number;
|
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
|
onDelete?: () => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateDialogProps) => {
|
export const TemplateDeleteDialog = ({
|
||||||
const router = useRouter();
|
id,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onDelete,
|
||||||
|
}: TemplateDeleteDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { mutateAsync: deleteTemplate, isPending } = trpcReact.template.deleteTemplate.useMutation({
|
const { mutateAsync: deleteTemplate, isPending } = trpcReact.template.deleteTemplate.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: async () => {
|
||||||
router.refresh();
|
await onDelete?.();
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Template deleted`),
|
title: _(msg`Template deleted`),
|
||||||
@ -1,14 +1,12 @@
|
|||||||
'use client';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Recipient, Template, TemplateDirectLink } from '@prisma/client';
|
||||||
import { Trans } from '@lingui/macro';
|
|
||||||
import { LinkIcon } from 'lucide-react';
|
import { LinkIcon } from 'lucide-react';
|
||||||
|
|
||||||
import type { Recipient, Template, TemplateDirectLink } from '@documenso/prisma/client';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
import { TemplateDirectLinkDialog } from '../template-direct-link-dialog';
|
import { TemplateDirectLinkDialog } from '~/components/dialogs/template-direct-link-dialog';
|
||||||
|
|
||||||
export type TemplateDirectLinkDialogWrapperProps = {
|
export type TemplateDirectLinkDialogWrapperProps = {
|
||||||
template: Template & { directLink?: TemplateDirectLink | null; recipients: Recipient[] };
|
template: Template & { directLink?: TemplateDirectLink | null; recipients: Recipient[] };
|
||||||
@ -1,11 +1,16 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import {
|
||||||
|
type Recipient,
|
||||||
|
RecipientRole,
|
||||||
|
type Template,
|
||||||
|
type TemplateDirectLink,
|
||||||
|
} from '@prisma/client';
|
||||||
import { CircleDotIcon, CircleIcon, ClipboardCopyIcon, InfoIcon, LoaderIcon } from 'lucide-react';
|
import { CircleDotIcon, CircleIcon, ClipboardCopyIcon, InfoIcon, LoaderIcon } from 'lucide-react';
|
||||||
|
import { Link, useRevalidator } from 'react-router';
|
||||||
import { P, match } from 'ts-pattern';
|
import { P, match } from 'ts-pattern';
|
||||||
|
|
||||||
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||||
@ -14,12 +19,6 @@ import { DIRECT_TEMPLATE_RECIPIENT_EMAIL } from '@documenso/lib/constants/direct
|
|||||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||||
import { DIRECT_TEMPLATE_DOCUMENTATION } from '@documenso/lib/constants/template';
|
import { DIRECT_TEMPLATE_DOCUMENTATION } from '@documenso/lib/constants/template';
|
||||||
import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
|
import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
|
||||||
import {
|
|
||||||
type Recipient,
|
|
||||||
RecipientRole,
|
|
||||||
type Template,
|
|
||||||
type TemplateDirectLink,
|
|
||||||
} from '@documenso/prisma/client';
|
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
|
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
@ -65,9 +64,9 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { quota, remaining } = useLimits();
|
const { quota, remaining } = useLimits();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const [, copy] = useCopyToClipboard();
|
const [, copy] = useCopyToClipboard();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [isEnabled, setIsEnabled] = useState(template.directLink?.enabled ?? false);
|
const [isEnabled, setIsEnabled] = useState(template.directLink?.enabled ?? false);
|
||||||
const [token, setToken] = useState(template.directLink?.token ?? null);
|
const [token, setToken] = useState(template.directLink?.token ?? null);
|
||||||
@ -77,11 +76,7 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const validDirectTemplateRecipients = useMemo(
|
const validDirectTemplateRecipients = useMemo(
|
||||||
() =>
|
() => template.recipients.filter((recipient) => recipient.role !== RecipientRole.CC),
|
||||||
template.recipients.filter(
|
|
||||||
(recipient) =>
|
|
||||||
recipient.role !== RecipientRole.CC && recipient.role !== RecipientRole.ASSISTANT,
|
|
||||||
),
|
|
||||||
[template.recipients],
|
[template.recipients],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -90,12 +85,12 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
isPending: isCreatingTemplateDirectLink,
|
isPending: isCreatingTemplateDirectLink,
|
||||||
reset: resetCreateTemplateDirectLink,
|
reset: resetCreateTemplateDirectLink,
|
||||||
} = trpcReact.template.createTemplateDirectLink.useMutation({
|
} = trpcReact.template.createTemplateDirectLink.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: async (data) => {
|
||||||
|
await revalidate();
|
||||||
|
|
||||||
setToken(data.token);
|
setToken(data.token);
|
||||||
setIsEnabled(data.enabled);
|
setIsEnabled(data.enabled);
|
||||||
setCurrentStep('MANAGE');
|
setCurrentStep('MANAGE');
|
||||||
|
|
||||||
router.refresh();
|
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
setSelectedRecipientId(null);
|
setSelectedRecipientId(null);
|
||||||
@ -110,7 +105,9 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
|
|
||||||
const { mutateAsync: toggleTemplateDirectLink, isPending: isTogglingTemplateAccess } =
|
const { mutateAsync: toggleTemplateDirectLink, isPending: isTogglingTemplateAccess } =
|
||||||
trpcReact.template.toggleTemplateDirectLink.useMutation({
|
trpcReact.template.toggleTemplateDirectLink.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: async (data) => {
|
||||||
|
await revalidate();
|
||||||
|
|
||||||
const enabledDescription = msg`Direct link signing has been enabled`;
|
const enabledDescription = msg`Direct link signing has been enabled`;
|
||||||
const disabledDescription = msg`Direct link signing has been disabled`;
|
const disabledDescription = msg`Direct link signing has been disabled`;
|
||||||
|
|
||||||
@ -133,7 +130,9 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
|
|
||||||
const { mutateAsync: deleteTemplateDirectLink, isPending: isDeletingTemplateDirectLink } =
|
const { mutateAsync: deleteTemplateDirectLink, isPending: isDeletingTemplateDirectLink } =
|
||||||
trpcReact.template.deleteTemplateDirectLink.useMutation({
|
trpcReact.template.deleteTemplateDirectLink.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: async () => {
|
||||||
|
await revalidate();
|
||||||
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
setToken(null);
|
setToken(null);
|
||||||
|
|
||||||
@ -143,7 +142,6 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
|
||||||
setToken(null);
|
setToken(null);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
@ -235,7 +233,7 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
templates.{' '}
|
templates.{' '}
|
||||||
<Link
|
<Link
|
||||||
className="mt-1 block underline underline-offset-4"
|
className="mt-1 block underline underline-offset-4"
|
||||||
href="/settings/billing"
|
to="/settings/billing"
|
||||||
>
|
>
|
||||||
Upgrade your account to continue!
|
Upgrade your account to continue!
|
||||||
</Link>
|
</Link>
|
||||||
@ -436,7 +434,7 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
await toggleTemplateDirectLink({
|
await toggleTemplateDirectLink({
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
enabled: isEnabled,
|
enabled: isEnabled,
|
||||||
}).catch((e) => null);
|
}).catch(() => null);
|
||||||
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
}}
|
}}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import { useRouter } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -15,28 +14,23 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
type DuplicateTemplateDialogProps = {
|
type TemplateDuplicateDialogProps = {
|
||||||
id: number;
|
id: number;
|
||||||
teamId?: number;
|
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DuplicateTemplateDialog = ({
|
export const TemplateDuplicateDialog = ({
|
||||||
id,
|
id,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: DuplicateTemplateDialogProps) => {
|
}: TemplateDuplicateDialogProps) => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { mutateAsync: duplicateTemplate, isPending } =
|
const { mutateAsync: duplicateTemplate, isPending } =
|
||||||
trpcReact.template.duplicateTemplate.useMutation({
|
trpcReact.template.duplicateTemplate.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
router.refresh();
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Template duplicated`),
|
title: _(msg`Template duplicated`),
|
||||||
description: _(msg`Your template has been duplicated successfully.`),
|
description: _(msg`Your template has been duplicated successfully.`),
|
||||||
@ -1,13 +1,12 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
|
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -28,29 +27,46 @@ import {
|
|||||||
} from '@documenso/ui/primitives/select';
|
} from '@documenso/ui/primitives/select';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
type MoveTemplateDialogProps = {
|
type TemplateMoveDialogProps = {
|
||||||
templateId: number;
|
templateId: number;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
|
onMove?: ({
|
||||||
|
templateId,
|
||||||
|
teamUrl,
|
||||||
|
}: {
|
||||||
|
templateId: number;
|
||||||
|
teamUrl: string;
|
||||||
|
}) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTemplateDialogProps) => {
|
export const TemplateMoveDialog = ({
|
||||||
const router = useRouter();
|
templateId,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onMove,
|
||||||
|
}: TemplateMoveDialogProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
|
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
|
||||||
|
|
||||||
const { data: teams, isPending: isLoadingTeams } = trpc.team.getTeams.useQuery();
|
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
|
||||||
|
|
||||||
const { mutateAsync: moveTemplate, isPending } = trpc.template.moveTemplateToTeam.useMutation({
|
const { mutateAsync: moveTemplate, isPending } = trpc.template.moveTemplateToTeam.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: async () => {
|
||||||
router.refresh();
|
const team = teams?.find((team) => team.id === selectedTeamId);
|
||||||
|
|
||||||
|
if (team) {
|
||||||
|
await onMove?.({ templateId, teamUrl: team.url });
|
||||||
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Template moved`),
|
title: _(msg`Template moved`),
|
||||||
description: _(msg`The template has been successfully moved to the selected team.`),
|
description: _(msg`The template has been successfully moved to the selected team.`),
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
@ -73,7 +89,7 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onMove = async () => {
|
const handleOnMove = async () => {
|
||||||
if (!selectedTeamId) {
|
if (!selectedTeamId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -108,9 +124,7 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Avatar className="h-8 w-8">
|
<Avatar className="h-8 w-8">
|
||||||
{team.avatarImageId && (
|
{team.avatarImageId && (
|
||||||
<AvatarImage
|
<AvatarImage src={formatAvatarUrl(team.avatarImageId)} />
|
||||||
src={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${team.avatarImageId}`}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<AvatarFallback className="text-sm text-gray-400">
|
<AvatarFallback className="text-sm text-gray-400">
|
||||||
@ -130,7 +144,11 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
|
|||||||
<Button variant="secondary" onClick={() => onOpenChange(false)}>
|
<Button variant="secondary" onClick={() => onOpenChange(false)}>
|
||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onMove} loading={isPending} disabled={!selectedTeamId || isPending}>
|
<Button
|
||||||
|
onClick={handleOnMove}
|
||||||
|
loading={isPending}
|
||||||
|
disabled={!selectedTeamId || isPending}
|
||||||
|
>
|
||||||
{isPending ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
|
{isPending ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@ -1,14 +1,14 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Recipient } from '@prisma/client';
|
||||||
|
import { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/client';
|
||||||
import { InfoIcon, Plus, Upload, X } from 'lucide-react';
|
import { InfoIcon, Plus, Upload, X } from 'lucide-react';
|
||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||||
@ -18,8 +18,6 @@ import {
|
|||||||
} from '@documenso/lib/constants/template';
|
} from '@documenso/lib/constants/template';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
|
||||||
import { DocumentDistributionMethod, DocumentSigningOrder } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -94,7 +92,7 @@ const ZAddRecipientsForNewDocumentSchema = z
|
|||||||
|
|
||||||
type TAddRecipientsForNewDocumentSchema = z.infer<typeof ZAddRecipientsForNewDocumentSchema>;
|
type TAddRecipientsForNewDocumentSchema = z.infer<typeof ZAddRecipientsForNewDocumentSchema>;
|
||||||
|
|
||||||
export type UseTemplateDialogProps = {
|
export type TemplateUseDialogProps = {
|
||||||
templateId: number;
|
templateId: number;
|
||||||
templateSigningOrder?: DocumentSigningOrder | null;
|
templateSigningOrder?: DocumentSigningOrder | null;
|
||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
@ -103,19 +101,19 @@ export type UseTemplateDialogProps = {
|
|||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function UseTemplateDialog({
|
export function TemplateUseDialog({
|
||||||
recipients,
|
recipients,
|
||||||
documentDistributionMethod = DocumentDistributionMethod.EMAIL,
|
documentDistributionMethod = DocumentDistributionMethod.EMAIL,
|
||||||
documentRootPath,
|
documentRootPath,
|
||||||
templateId,
|
templateId,
|
||||||
templateSigningOrder,
|
templateSigningOrder,
|
||||||
trigger,
|
trigger,
|
||||||
}: UseTemplateDialogProps) {
|
}: TemplateUseDialogProps) {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const form = useForm<TAddRecipientsForNewDocumentSchema>({
|
const form = useForm<TAddRecipientsForNewDocumentSchema>({
|
||||||
@ -179,7 +177,7 @@ export function UseTemplateDialog({
|
|||||||
documentPath += '?action=view-signing-links';
|
documentPath += '?action=view-signing-links';
|
||||||
}
|
}
|
||||||
|
|
||||||
router.push(documentPath);
|
await navigate(documentPath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -1,16 +1,13 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { ApiToken } from '@prisma/client';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { ApiToken } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -33,35 +30,33 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DeleteTokenDialogProps = {
|
export type TokenDeleteDialogProps = {
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
token: Pick<ApiToken, 'id' | 'name'>;
|
token: Pick<ApiToken, 'id' | 'name'>;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DeleteTokenDialog({
|
export default function TokenDeleteDialog({
|
||||||
teamId,
|
teamId,
|
||||||
token,
|
token,
|
||||||
onDelete,
|
onDelete,
|
||||||
children,
|
children,
|
||||||
}: DeleteTokenDialogProps) {
|
}: TokenDeleteDialogProps) {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const deleteMessage = _(msg`delete ${token.name}`);
|
const deleteMessage = _(msg`delete ${token.name}`);
|
||||||
|
|
||||||
const ZDeleteTokenDialogSchema = z.object({
|
const ZTokenDeleteDialogSchema = z.object({
|
||||||
tokenName: z.literal(deleteMessage, {
|
tokenName: z.literal(deleteMessage, {
|
||||||
errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
|
errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
type TDeleteTokenByIdMutationSchema = z.infer<typeof ZDeleteTokenDialogSchema>;
|
type TDeleteTokenByIdMutationSchema = z.infer<typeof ZTokenDeleteDialogSchema>;
|
||||||
|
|
||||||
const { mutateAsync: deleteTokenMutation } = trpc.apiToken.deleteTokenById.useMutation({
|
const { mutateAsync: deleteTokenMutation } = trpc.apiToken.deleteTokenById.useMutation({
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
@ -70,7 +65,7 @@ export default function DeleteTokenDialog({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<TDeleteTokenByIdMutationSchema>({
|
const form = useForm<TDeleteTokenByIdMutationSchema>({
|
||||||
resolver: zodResolver(ZDeleteTokenDialogSchema),
|
resolver: zodResolver(ZTokenDeleteDialogSchema),
|
||||||
values: {
|
values: {
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
},
|
},
|
||||||
@ -90,8 +85,6 @@ export default function DeleteTokenDialog({
|
|||||||
});
|
});
|
||||||
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
|
||||||
router.refresh();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
@ -1,12 +1,9 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
@ -39,22 +36,20 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
import { TriggerMultiSelectCombobox } from './trigger-multiselect-combobox';
|
import { WebhookMultiSelectCombobox } from '../general/webhook-multiselect-combobox';
|
||||||
|
|
||||||
const ZCreateWebhookFormSchema = ZCreateWebhookMutationSchema.omit({ teamId: true });
|
const ZCreateWebhookFormSchema = ZCreateWebhookMutationSchema.omit({ teamId: true });
|
||||||
|
|
||||||
type TCreateWebhookFormSchema = z.infer<typeof ZCreateWebhookFormSchema>;
|
type TCreateWebhookFormSchema = z.infer<typeof ZCreateWebhookFormSchema>;
|
||||||
|
|
||||||
export type CreateWebhookDialogProps = {
|
export type WebhookCreateDialogProps = {
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
|
|
||||||
export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogProps) => {
|
export const WebhookCreateDialog = ({ trigger, ...props }: WebhookCreateDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const team = useOptionalCurrentTeam();
|
const team = useOptionalCurrentTeam();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@ -94,8 +89,6 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr
|
|||||||
});
|
});
|
||||||
|
|
||||||
form.reset();
|
form.reset();
|
||||||
|
|
||||||
router.refresh();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
@ -191,7 +184,7 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr
|
|||||||
<Trans>Triggers</Trans>
|
<Trans>Triggers</Trans>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<TriggerMultiSelectCombobox
|
<WebhookMultiSelectCombobox
|
||||||
listValues={value}
|
listValues={value}
|
||||||
onChange={(values: string[]) => {
|
onChange={(values: string[]) => {
|
||||||
onChange(values);
|
onChange(values);
|
||||||
@ -1,16 +1,13 @@
|
|||||||
'use effect';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Webhook } from '@prisma/client';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { Webhook } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -35,18 +32,16 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
export type DeleteWebhookDialogProps = {
|
export type WebhookDeleteDialogProps = {
|
||||||
webhook: Pick<Webhook, 'id' | 'webhookUrl'>;
|
webhook: Pick<Webhook, 'id' | 'webhookUrl'>;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogProps) => {
|
export const WebhookDeleteDialog = ({ webhook, children }: WebhookDeleteDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const team = useOptionalCurrentTeam();
|
const team = useOptionalCurrentTeam();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@ -81,8 +76,6 @@ export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogPr
|
|||||||
});
|
});
|
||||||
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|
||||||
router.refresh();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
@ -1,20 +1,23 @@
|
|||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
|
||||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||||
|
|
||||||
import { Logo } from '~/components/branding/logo';
|
|
||||||
import { SignInForm } from '~/components/forms/signin';
|
import { SignInForm } from '~/components/forms/signin';
|
||||||
|
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||||
|
|
||||||
export type EmbedAuthenticateViewProps = {
|
export type EmbedAuthenticationRequiredProps = {
|
||||||
email?: string;
|
email?: string;
|
||||||
returnTo: string;
|
returnTo: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EmbedAuthenticateView = ({ email, returnTo }: EmbedAuthenticateViewProps) => {
|
export const EmbedAuthenticationRequired = ({
|
||||||
|
email,
|
||||||
|
returnTo,
|
||||||
|
}: EmbedAuthenticationRequiredProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[100dvh] w-full items-center justify-center">
|
<div className="flex min-h-[100dvh] w-full items-center justify-center">
|
||||||
<div className="flex w-full max-w-md flex-col">
|
<div className="flex w-full max-w-md flex-col">
|
||||||
<Logo className="h-8" />
|
<BrandingLogo className="h-8" />
|
||||||
|
|
||||||
<Alert className="mt-8" variant="warning">
|
<Alert className="mt-8" variant="warning">
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
@ -1,21 +1,19 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { type DocumentData, type Field, FieldType } from '@prisma/client';
|
||||||
|
import type { DocumentMeta, Recipient, Signature, TemplateMeta } from '@prisma/client';
|
||||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { useSearchParams } from 'react-router';
|
||||||
|
|
||||||
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||||
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import type { DocumentMeta, Recipient, Signature, TemplateMeta } from '@documenso/prisma/client';
|
|
||||||
import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type {
|
import type {
|
||||||
TRemovedSignedFieldWithTokenMutationSchema,
|
TRemovedSignedFieldWithTokenMutationSchema,
|
||||||
@ -31,15 +29,15 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
|||||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import type { DirectTemplateLocalField } from '~/app/(recipient)/d/[token]/sign-direct-template';
|
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
import { ZDirectTemplateEmbedDataSchema } from '~/types/embed-direct-template-schema';
|
||||||
import { Logo } from '~/components/branding/logo';
|
import { injectCss } from '~/utils/css-vars';
|
||||||
|
|
||||||
import { EmbedClientLoading } from '../../client-loading';
|
import type { DirectTemplateLocalField } from '../general/direct-template/direct-template-signing-form';
|
||||||
import { EmbedDocumentCompleted } from '../../completed';
|
import { useRequiredDocumentSigningContext } from '../general/document-signing/document-signing-provider';
|
||||||
import { EmbedDocumentFields } from '../../document-fields';
|
import { EmbedClientLoading } from './embed-client-loading';
|
||||||
import { injectCss } from '../../util';
|
import { EmbedDocumentCompleted } from './embed-document-completed';
|
||||||
import { ZDirectTemplateEmbedDataSchema } from './schema';
|
import { EmbedDocumentFields } from './embed-document-fields';
|
||||||
|
|
||||||
export type EmbedDirectTemplateClientPageProps = {
|
export type EmbedDirectTemplateClientPageProps = {
|
||||||
token: string;
|
token: string;
|
||||||
@ -65,7 +63,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
fullName,
|
fullName,
|
||||||
@ -76,7 +74,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
setEmail,
|
setEmail,
|
||||||
setSignature,
|
setSignature,
|
||||||
setSignatureValid,
|
setSignatureValid,
|
||||||
} = useRequiredSigningContext();
|
} = useRequiredDocumentSigningContext();
|
||||||
|
|
||||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||||
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
||||||
@ -485,6 +483,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
|
|
||||||
{/* Fields */}
|
{/* Fields */}
|
||||||
<EmbedDocumentFields
|
<EmbedDocumentFields
|
||||||
|
recipient={recipient}
|
||||||
fields={localFields}
|
fields={localFields}
|
||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
@ -495,7 +494,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
{!hidePoweredBy && (
|
{!hidePoweredBy && (
|
||||||
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
|
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
|
||||||
<span>Powered by</span>
|
<span>Powered by</span>
|
||||||
<Logo className="ml-2 inline-block h-[14px]" />
|
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Signature } from '@prisma/client';
|
||||||
|
|
||||||
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
|
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
|
||||||
import type { Signature } from '@documenso/prisma/client';
|
|
||||||
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
||||||
|
|
||||||
export type EmbedDocumentCompletedPageProps = {
|
export type EmbedDocumentCompletedPageProps = {
|
||||||
@ -10,8 +10,9 @@ export type EmbedDocumentCompletedPageProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const EmbedDocumentCompleted = ({ name, signature }: EmbedDocumentCompletedPageProps) => {
|
export const EmbedDocumentCompleted = ({ name, signature }: EmbedDocumentCompletedPageProps) => {
|
||||||
|
console.log({ signature });
|
||||||
return (
|
return (
|
||||||
<div className="embed--DocumentCompleted relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||||
<h3 className="text-foreground text-2xl font-semibold">
|
<h3 className="text-foreground text-2xl font-semibold">
|
||||||
<Trans>Document Completed!</Trans>
|
<Trans>Document Completed!</Trans>
|
||||||
</h3>
|
</h3>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
'use client';
|
import type { DocumentMeta, Recipient, TemplateMeta } from '@prisma/client';
|
||||||
|
import { type Field, FieldType } from '@prisma/client';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||||
@ -12,8 +12,6 @@ import {
|
|||||||
ZRadioFieldMeta,
|
ZRadioFieldMeta,
|
||||||
ZTextFieldMeta,
|
ZTextFieldMeta,
|
||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
import type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client';
|
|
||||||
import { type Field, FieldType } from '@documenso/prisma/client';
|
|
||||||
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
||||||
import type {
|
import type {
|
||||||
TRemovedSignedFieldWithTokenMutationSchema,
|
TRemovedSignedFieldWithTokenMutationSchema,
|
||||||
@ -21,18 +19,19 @@ import type {
|
|||||||
} from '@documenso/trpc/server/field-router/schema';
|
} from '@documenso/trpc/server/field-router/schema';
|
||||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||||
|
|
||||||
import { CheckboxField } from '~/app/(signing)/sign/[token]/checkbox-field';
|
import { DocumentSigningCheckboxField } from '~/components/general/document-signing/document-signing-checkbox-field';
|
||||||
import { DateField } from '~/app/(signing)/sign/[token]/date-field';
|
import { DocumentSigningDateField } from '~/components/general/document-signing/document-signing-date-field';
|
||||||
import { DropdownField } from '~/app/(signing)/sign/[token]/dropdown-field';
|
import { DocumentSigningDropdownField } from '~/components/general/document-signing/document-signing-dropdown-field';
|
||||||
import { EmailField } from '~/app/(signing)/sign/[token]/email-field';
|
import { DocumentSigningEmailField } from '~/components/general/document-signing/document-signing-email-field';
|
||||||
import { InitialsField } from '~/app/(signing)/sign/[token]/initials-field';
|
import { DocumentSigningInitialsField } from '~/components/general/document-signing/document-signing-initials-field';
|
||||||
import { NameField } from '~/app/(signing)/sign/[token]/name-field';
|
import { DocumentSigningNameField } from '~/components/general/document-signing/document-signing-name-field';
|
||||||
import { NumberField } from '~/app/(signing)/sign/[token]/number-field';
|
import { DocumentSigningNumberField } from '~/components/general/document-signing/document-signing-number-field';
|
||||||
import { RadioField } from '~/app/(signing)/sign/[token]/radio-field';
|
import { DocumentSigningRadioField } from '~/components/general/document-signing/document-signing-radio-field';
|
||||||
import { SignatureField } from '~/app/(signing)/sign/[token]/signature-field';
|
import { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-field';
|
||||||
import { TextField } from '~/app/(signing)/sign/[token]/text-field';
|
import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
|
||||||
|
|
||||||
export type EmbedDocumentFieldsProps = {
|
export type EmbedDocumentFieldsProps = {
|
||||||
|
recipient: Recipient;
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
metadata?: DocumentMeta | TemplateMeta | null;
|
metadata?: DocumentMeta | TemplateMeta | null;
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
@ -40,6 +39,7 @@ export type EmbedDocumentFieldsProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const EmbedDocumentFields = ({
|
export const EmbedDocumentFields = ({
|
||||||
|
recipient,
|
||||||
fields,
|
fields,
|
||||||
metadata,
|
metadata,
|
||||||
onSignField,
|
onSignField,
|
||||||
@ -50,34 +50,38 @@ export const EmbedDocumentFields = ({
|
|||||||
{fields.map((field) =>
|
{fields.map((field) =>
|
||||||
match(field.type)
|
match(field.type)
|
||||||
.with(FieldType.SIGNATURE, () => (
|
.with(FieldType.SIGNATURE, () => (
|
||||||
<SignatureField
|
<DocumentSigningSignatureField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
|
recipient={recipient}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
typedSignatureEnabled={metadata?.typedSignatureEnabled}
|
typedSignatureEnabled={metadata?.typedSignatureEnabled}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.INITIALS, () => (
|
.with(FieldType.INITIALS, () => (
|
||||||
<InitialsField
|
<DocumentSigningInitialsField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
|
recipient={recipient}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.NAME, () => (
|
.with(FieldType.NAME, () => (
|
||||||
<NameField
|
<DocumentSigningNameField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
|
recipient={recipient}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.DATE, () => (
|
.with(FieldType.DATE, () => (
|
||||||
<DateField
|
<DocumentSigningDateField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
|
recipient={recipient}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
dateFormat={metadata?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
|
dateFormat={metadata?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
|
||||||
@ -85,9 +89,10 @@ export const EmbedDocumentFields = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.EMAIL, () => (
|
.with(FieldType.EMAIL, () => (
|
||||||
<EmailField
|
<DocumentSigningEmailField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
|
recipient={recipient}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@ -99,9 +104,10 @@ export const EmbedDocumentFields = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<DocumentSigningTextField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={fieldWithMeta}
|
field={fieldWithMeta}
|
||||||
|
recipient={recipient}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@ -114,9 +120,10 @@ export const EmbedDocumentFields = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NumberField
|
<DocumentSigningNumberField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={fieldWithMeta}
|
field={fieldWithMeta}
|
||||||
|
recipient={recipient}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@ -129,9 +136,10 @@ export const EmbedDocumentFields = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RadioField
|
<DocumentSigningRadioField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={fieldWithMeta}
|
field={fieldWithMeta}
|
||||||
|
recipient={recipient}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@ -144,9 +152,10 @@ export const EmbedDocumentFields = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CheckboxField
|
<DocumentSigningCheckboxField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={fieldWithMeta}
|
field={fieldWithMeta}
|
||||||
|
recipient={recipient}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@ -159,9 +168,10 @@ export const EmbedDocumentFields = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownField
|
<DocumentSigningDropdownField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={fieldWithMeta}
|
field={fieldWithMeta}
|
||||||
|
recipient={recipient}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
376
apps/remix/app/components/embed/embed-document-signing-page.tsx
Normal file
376
apps/remix/app/components/embed/embed-document-signing-page.tsx
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { DocumentMeta, Recipient, TemplateMeta } from '@prisma/client';
|
||||||
|
import { type DocumentData, type Field, FieldType } from '@prisma/client';
|
||||||
|
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||||
|
|
||||||
|
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||||
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
|
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
|
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||||
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
|
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||||
|
import { injectCss } from '~/utils/css-vars';
|
||||||
|
|
||||||
|
import { ZSignDocumentEmbedDataSchema } from '../../types/embed-document-sign-schema';
|
||||||
|
import { useRequiredDocumentSigningContext } from '../general/document-signing/document-signing-provider';
|
||||||
|
import { EmbedClientLoading } from './embed-client-loading';
|
||||||
|
import { EmbedDocumentCompleted } from './embed-document-completed';
|
||||||
|
import { EmbedDocumentFields } from './embed-document-fields';
|
||||||
|
|
||||||
|
export type EmbedSignDocumentClientPageProps = {
|
||||||
|
token: string;
|
||||||
|
documentId: number;
|
||||||
|
documentData: DocumentData;
|
||||||
|
recipient: Recipient;
|
||||||
|
fields: Field[];
|
||||||
|
metadata?: DocumentMeta | TemplateMeta | null;
|
||||||
|
isCompleted?: boolean;
|
||||||
|
hidePoweredBy?: boolean;
|
||||||
|
isPlatformOrEnterprise?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmbedSignDocumentClientPage = ({
|
||||||
|
token,
|
||||||
|
documentId,
|
||||||
|
documentData,
|
||||||
|
recipient,
|
||||||
|
fields,
|
||||||
|
metadata,
|
||||||
|
isCompleted,
|
||||||
|
hidePoweredBy = false,
|
||||||
|
isPlatformOrEnterprise = false,
|
||||||
|
}: EmbedSignDocumentClientPageProps) => {
|
||||||
|
const { _ } = useLingui();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const {
|
||||||
|
fullName,
|
||||||
|
email,
|
||||||
|
signature,
|
||||||
|
signatureValid,
|
||||||
|
setFullName,
|
||||||
|
setSignature,
|
||||||
|
setSignatureValid,
|
||||||
|
} = useRequiredDocumentSigningContext();
|
||||||
|
|
||||||
|
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||||
|
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
||||||
|
const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
|
||||||
|
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
|
const [isNameLocked, setIsNameLocked] = useState(false);
|
||||||
|
|
||||||
|
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
||||||
|
|
||||||
|
const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500);
|
||||||
|
|
||||||
|
const [pendingFields, _completedFields] = [
|
||||||
|
fields.filter((field) => !field.inserted),
|
||||||
|
fields.filter((field) => field.inserted),
|
||||||
|
];
|
||||||
|
|
||||||
|
const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } =
|
||||||
|
trpc.recipient.completeDocumentWithToken.useMutation();
|
||||||
|
|
||||||
|
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
|
||||||
|
|
||||||
|
const onNextFieldClick = () => {
|
||||||
|
validateFieldsInserted(fields);
|
||||||
|
|
||||||
|
setShowPendingFieldTooltip(true);
|
||||||
|
setIsExpanded(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCompleteClick = async () => {
|
||||||
|
try {
|
||||||
|
if (hasSignatureField && !signatureValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid = validateFieldsInserted(fields);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
setShowPendingFieldTooltip(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await completeDocumentWithToken({
|
||||||
|
documentId,
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (window.parent) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: 'document-completed',
|
||||||
|
data: {
|
||||||
|
token,
|
||||||
|
documentId,
|
||||||
|
recipientId: recipient.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasCompletedDocument(true);
|
||||||
|
} catch (err) {
|
||||||
|
if (window.parent) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: 'document-error',
|
||||||
|
data: null,
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: _(msg`Something went wrong`),
|
||||||
|
description: _(
|
||||||
|
msg`We were unable to submit this document at this time. Please try again later.`,
|
||||||
|
),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const hash = window.location.hash.slice(1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = ZSignDocumentEmbedDataSchema.parse(JSON.parse(decodeURIComponent(atob(hash))));
|
||||||
|
|
||||||
|
if (!isCompleted && data.name) {
|
||||||
|
setFullName(data.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since a recipient can be provided a name we can lock it without requiring
|
||||||
|
// a to be provided by the parent application, unlike direct templates.
|
||||||
|
setIsNameLocked(!!data.lockName);
|
||||||
|
|
||||||
|
if (data.darkModeDisabled) {
|
||||||
|
document.documentElement.classList.add('dark-mode-disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlatformOrEnterprise) {
|
||||||
|
injectCss({
|
||||||
|
css: data.css,
|
||||||
|
cssVars: data.cssVars,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasFinishedInit(true);
|
||||||
|
|
||||||
|
// !: While the two setters are stable we still want to ensure we're avoiding
|
||||||
|
// !: re-renders.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasFinishedInit && hasDocumentLoaded && window.parent) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: 'document-ready',
|
||||||
|
data: null,
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [hasFinishedInit, hasDocumentLoaded]);
|
||||||
|
|
||||||
|
if (hasCompletedDocument) {
|
||||||
|
return (
|
||||||
|
<EmbedDocumentCompleted
|
||||||
|
name={fullName}
|
||||||
|
signature={{
|
||||||
|
id: 1,
|
||||||
|
fieldId: 1,
|
||||||
|
recipientId: 1,
|
||||||
|
created: new Date(),
|
||||||
|
signatureImageAsBase64: signature?.startsWith('data:') ? signature : null,
|
||||||
|
typedSignature: signature?.startsWith('data:') ? null : signature,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||||
|
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
|
||||||
|
|
||||||
|
<div className="relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
|
||||||
|
{/* Viewer */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<LazyPDFViewer
|
||||||
|
documentData={documentData}
|
||||||
|
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Widget */}
|
||||||
|
<div
|
||||||
|
key={isExpanded ? 'expanded' : 'collapsed'}
|
||||||
|
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
|
||||||
|
data-expanded={isExpanded || undefined}
|
||||||
|
>
|
||||||
|
<div className="border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between gap-x-2">
|
||||||
|
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
|
||||||
|
<Trans>Sign document</Trans>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
||||||
|
{isExpanded ? (
|
||||||
|
<LucideChevronDown
|
||||||
|
className="text-muted-foreground h-5 w-5"
|
||||||
|
onClick={() => setIsExpanded(false)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<LucideChevronUp
|
||||||
|
className="text-muted-foreground h-5 w-5"
|
||||||
|
onClick={() => setIsExpanded(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden group-data-[expanded]/document-widget:block md:block">
|
||||||
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
|
<Trans>Sign the document to complete the process.</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr className="border-border mb-8 mt-4" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<div className="-mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
|
||||||
|
<div className="flex flex-1 flex-col gap-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="full-name">
|
||||||
|
<Trans>Full Name</Trans>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="full-name"
|
||||||
|
className="bg-background mt-2"
|
||||||
|
disabled={isNameLocked}
|
||||||
|
value={fullName}
|
||||||
|
onChange={(e) => !isNameLocked && setFullName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="email">
|
||||||
|
<Trans>Email</Trans>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
className="bg-background mt-2"
|
||||||
|
value={email}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="Signature">
|
||||||
|
<Trans>Signature</Trans>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Card className="mt-2" gradient degrees={-120}>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<SignaturePad
|
||||||
|
className="h-44 w-full"
|
||||||
|
disabled={isThrottled || isSubmitting}
|
||||||
|
defaultValue={signature ?? undefined}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSignature(value);
|
||||||
|
}}
|
||||||
|
onValidityChange={(isValid) => {
|
||||||
|
setSignatureValid(isValid);
|
||||||
|
}}
|
||||||
|
allowTypedSignature={Boolean(
|
||||||
|
metadata &&
|
||||||
|
'typedSignatureEnabled' in metadata &&
|
||||||
|
metadata.typedSignatureEnabled,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{hasSignatureField && !signatureValid && (
|
||||||
|
<div className="text-destructive mt-2 text-sm">
|
||||||
|
<Trans>
|
||||||
|
Signature is too small. Please provide a more complete signature.
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden flex-1 group-data-[expanded]/document-widget:block md:block" />
|
||||||
|
|
||||||
|
<div className="mt-4 hidden w-full grid-cols-2 items-center group-data-[expanded]/document-widget:grid md:grid">
|
||||||
|
{pendingFields.length > 0 ? (
|
||||||
|
<Button className="col-start-2" onClick={() => onNextFieldClick()}>
|
||||||
|
<Trans>Next</Trans>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
className="col-start-2"
|
||||||
|
disabled={isThrottled || (hasSignatureField && !signatureValid)}
|
||||||
|
loading={isSubmitting}
|
||||||
|
onClick={() => throttledOnCompleteClick()}
|
||||||
|
>
|
||||||
|
<Trans>Complete</Trans>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||||
|
{showPendingFieldTooltip && pendingFields.length > 0 && (
|
||||||
|
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning">
|
||||||
|
<Trans>Click to insert field</Trans>
|
||||||
|
</FieldToolTip>
|
||||||
|
)}
|
||||||
|
</ElementVisible>
|
||||||
|
|
||||||
|
{/* Fields */}
|
||||||
|
<EmbedDocumentFields recipient={recipient} fields={fields} metadata={metadata} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!hidePoweredBy && (
|
||||||
|
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
|
||||||
|
<span>Powered by</span>
|
||||||
|
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,14 +1,12 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { flushSync } from 'react-dom';
|
import { flushSync } from 'react-dom';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
@ -42,10 +40,9 @@ export const ZDisable2FAForm = z.object({
|
|||||||
export type TDisable2FAForm = z.infer<typeof ZDisable2FAForm>;
|
export type TDisable2FAForm = z.infer<typeof ZDisable2FAForm>;
|
||||||
|
|
||||||
export const DisableAuthenticatorAppDialog = () => {
|
export const DisableAuthenticatorAppDialog = () => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [twoFactorDisableMethod, setTwoFactorDisableMethod] = useState<'totp' | 'backup'>('totp');
|
const [twoFactorDisableMethod, setTwoFactorDisableMethod] = useState<'totp' | 'backup'>('totp');
|
||||||
@ -97,7 +94,7 @@ export const DisableAuthenticatorAppDialog = () => {
|
|||||||
onCloseTwoFactorDisableDialog();
|
onCloseTwoFactorDisableDialog();
|
||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
await revalidate();
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Unable to disable two-factor authentication`),
|
title: _(msg`Unable to disable two-factor authentication`),
|
||||||
@ -1,13 +1,11 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
import { renderSVG } from 'uqr';
|
import { renderSVG } from 'uqr';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@ -50,8 +48,7 @@ export type EnableAuthenticatorAppDialogProps = {
|
|||||||
export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorAppDialogProps) => {
|
export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorAppDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [recoveryCodes, setRecoveryCodes] = useState<string[] | null>(null);
|
const [recoveryCodes, setRecoveryCodes] = useState<string[] | null>(null);
|
||||||
@ -133,7 +130,7 @@ export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorA
|
|||||||
|
|
||||||
if (!isOpen && recoveryCodes && recoveryCodes.length > 0) {
|
if (!isOpen && recoveryCodes && recoveryCodes.length > 0) {
|
||||||
setRecoveryCodes(null);
|
setRecoveryCodes(null);
|
||||||
router.refresh();
|
void revalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Copy } from 'lucide-react';
|
import { Copy } from 'lucide-react';
|
||||||
|
|
||||||
@ -1,16 +1,13 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -147,7 +144,7 @@ export const ViewRecoveryCodesDialog = () => {
|
|||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{match(AppError.parseError(error).message)
|
{match(AppError.parseError(error).message)
|
||||||
.with(ErrorCode.INCORRECT_TWO_FACTOR_CODE, () => (
|
.with('INCORRECT_TWO_FACTOR_CODE', () => (
|
||||||
<Trans>Invalid code. Please try again.</Trans>
|
<Trans>Invalid code. Please try again.</Trans>
|
||||||
))
|
))
|
||||||
.otherwise(() => (
|
.otherwise(() => (
|
||||||
@ -1,22 +1,20 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { ErrorCode, useDropzone } from 'react-dropzone';
|
import { ErrorCode, useDropzone } from 'react-dropzone';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { base64 } from '@documenso/lib/universal/base64';
|
import { base64 } from '@documenso/lib/universal/base64';
|
||||||
|
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { Team, User } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||||
@ -31,6 +29,8 @@ import {
|
|||||||
} from '@documenso/ui/primitives/form/form';
|
} from '@documenso/ui/primitives/form/form';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
export const ZAvatarImageFormSchema = z.object({
|
export const ZAvatarImageFormSchema = z.object({
|
||||||
bytes: z.string().nullish(),
|
bytes: z.string().nullish(),
|
||||||
});
|
});
|
||||||
@ -39,15 +39,15 @@ export type TAvatarImageFormSchema = z.infer<typeof ZAvatarImageFormSchema>;
|
|||||||
|
|
||||||
export type AvatarImageFormProps = {
|
export type AvatarImageFormProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
user: User;
|
|
||||||
team?: Team;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AvatarImageForm = ({ className, user, team }: AvatarImageFormProps) => {
|
export const AvatarImageForm = ({ className }: AvatarImageFormProps) => {
|
||||||
|
const { user } = useSession();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const router = useRouter();
|
const team = useOptionalCurrentTeam();
|
||||||
|
|
||||||
const { mutateAsync: setProfileImage } = trpc.profile.setProfileImage.useMutation();
|
const { mutateAsync: setProfileImage } = trpc.profile.setProfileImage.useMutation();
|
||||||
|
|
||||||
@ -109,7 +109,7 @@ export const AvatarImageForm = ({ className, user, team }: AvatarImageFormProps)
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
void revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -146,11 +146,7 @@ export const AvatarImageForm = ({ className, user, team }: AvatarImageFormProps)
|
|||||||
<div className="flex items-center gap-8">
|
<div className="flex items-center gap-8">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Avatar className="h-16 w-16 border-2 border-solid">
|
<Avatar className="h-16 w-16 border-2 border-solid">
|
||||||
{avatarImageId && (
|
{avatarImageId && <AvatarImage src={formatAvatarUrl(avatarImageId)} />}
|
||||||
<AvatarImage
|
|
||||||
src={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${avatarImageId}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<AvatarFallback className="text-sm text-gray-400">
|
<AvatarFallback className="text-sm text-gray-400">
|
||||||
{initials}
|
{initials}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
@ -1,14 +1,12 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -36,7 +34,7 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
|
|||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const form = useForm<TForgotPasswordFormSchema>({
|
const form = useForm<TForgotPasswordFormSchema>({
|
||||||
values: {
|
values: {
|
||||||
@ -47,10 +45,10 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
|
|||||||
|
|
||||||
const isSubmitting = form.formState.isSubmitting;
|
const isSubmitting = form.formState.isSubmitting;
|
||||||
|
|
||||||
const { mutateAsync: forgotPassword } = trpc.profile.forgotPassword.useMutation();
|
|
||||||
|
|
||||||
const onFormSubmit = async ({ email }: TForgotPasswordFormSchema) => {
|
const onFormSubmit = async ({ email }: TForgotPasswordFormSchema) => {
|
||||||
await forgotPassword({ email }).catch(() => null);
|
await authClient.emailPassword.forgotPassword({ email }).catch(() => null);
|
||||||
|
|
||||||
|
await navigate('/check-email');
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Reset email sent`),
|
title: _(msg`Reset email sent`),
|
||||||
@ -61,8 +59,6 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
form.reset();
|
form.reset();
|
||||||
|
|
||||||
router.push('/check-email');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -1,15 +1,14 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { authClient } from '@documenso/auth/client';
|
||||||
|
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import type { User } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { ZCurrentPasswordSchema, ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
import { ZCurrentPasswordSchema, ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -39,7 +38,7 @@ export type TPasswordFormSchema = z.infer<typeof ZPasswordFormSchema>;
|
|||||||
|
|
||||||
export type PasswordFormProps = {
|
export type PasswordFormProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
user: User;
|
user: SessionUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PasswordForm = ({ className }: PasswordFormProps) => {
|
export const PasswordForm = ({ className }: PasswordFormProps) => {
|
||||||
@ -57,11 +56,9 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
|
|||||||
|
|
||||||
const isSubmitting = form.formState.isSubmitting;
|
const isSubmitting = form.formState.isSubmitting;
|
||||||
|
|
||||||
const { mutateAsync: updatePassword } = trpc.profile.updatePassword.useMutation();
|
|
||||||
|
|
||||||
const onFormSubmit = async ({ currentPassword, password }: TPasswordFormSchema) => {
|
const onFormSubmit = async ({ currentPassword, password }: TPasswordFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await updatePassword({
|
await authClient.emailPassword.updatePassword({
|
||||||
currentPassword,
|
currentPassword,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
@ -1,14 +1,12 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { User } from '@documenso/prisma/client';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -39,14 +37,13 @@ export type TProfileFormSchema = z.infer<typeof ZProfileFormSchema>;
|
|||||||
|
|
||||||
export type ProfileFormProps = {
|
export type ProfileFormProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
user: User;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
export const ProfileForm = ({ className }: ProfileFormProps) => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { user } = useSession();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const form = useForm<TProfileFormSchema>({
|
const form = useForm<TProfileFormSchema>({
|
||||||
values: {
|
values: {
|
||||||
@ -73,7 +70,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
@ -1,19 +1,15 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import type { User } from '@prisma/client';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import profileClaimTeaserImage from '@documenso/assets/images/profile-claim-teaser.png';
|
import profileClaimTeaserImage from '@documenso/assets/images/profile-claim-teaser.png';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { User } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -35,7 +31,7 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { UserProfileSkeleton } from '../ui/user-profile-skeleton';
|
import { UserProfileSkeleton } from '../general/user-profile-skeleton';
|
||||||
|
|
||||||
export const ZClaimPublicProfileFormSchema = z.object({
|
export const ZClaimPublicProfileFormSchema = z.object({
|
||||||
url: z
|
url: z
|
||||||
@ -92,12 +88,12 @@ export const ClaimPublicProfileDialogForm = ({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
if (error.code === AppErrorCode.PROFILE_URL_TAKEN) {
|
if (error.code === 'PROFILE_URL_TAKEN') {
|
||||||
form.setError('url', {
|
form.setError('url', {
|
||||||
type: 'manual',
|
type: 'manual',
|
||||||
message: _(msg`This username is already taken`),
|
message: _(msg`This username is already taken`),
|
||||||
});
|
});
|
||||||
} else if (error.code === AppErrorCode.PREMIUM_PROFILE_URL) {
|
} else if (error.code === 'PREMIUM_PROFILE_URL') {
|
||||||
form.setError('url', {
|
form.setError('url', {
|
||||||
type: 'manual',
|
type: 'manual',
|
||||||
message: error.message,
|
message: error.message,
|
||||||
@ -135,7 +131,7 @@ export const ClaimPublicProfileDialogForm = ({
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<Image src={profileClaimTeaserImage} alt="profile claim teaser" />
|
<img src={profileClaimTeaserImage} alt="profile claim teaser" />
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@ -1,10 +1,10 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Plural, Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Plural, Trans } from '@lingui/react/macro';
|
||||||
|
import type { TeamProfile, UserProfile } from '@prisma/client';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import { CheckSquareIcon, CopyIcon } from 'lucide-react';
|
import { CheckSquareIcon, CopyIcon } from 'lucide-react';
|
||||||
@ -12,9 +12,8 @@ import { useForm } from 'react-hook-form';
|
|||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { formatUserProfilePath } from '@documenso/lib/utils/public-profiles';
|
import { formatUserProfilePath } from '@documenso/lib/utils/public-profiles';
|
||||||
import type { TeamProfile, UserProfile } from '@documenso/prisma/client';
|
|
||||||
import {
|
import {
|
||||||
MAX_PROFILE_BIO_LENGTH,
|
MAX_PROFILE_BIO_LENGTH,
|
||||||
ZUpdatePublicProfileMutationSchema,
|
ZUpdatePublicProfileMutationSchema,
|
||||||
@ -90,8 +89,8 @@ export const PublicProfileForm = ({
|
|||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
switch (error.code) {
|
switch (error.code) {
|
||||||
case AppErrorCode.PREMIUM_PROFILE_URL:
|
case 'PREMIUM_PROFILE_URL':
|
||||||
case AppErrorCode.PROFILE_URL_TAKEN:
|
case 'PROFILE_URL_TAKEN':
|
||||||
form.setError('url', {
|
form.setError('url', {
|
||||||
type: 'manual',
|
type: 'manual',
|
||||||
message: error.message,
|
message: error.message,
|
||||||
@ -1,16 +1,14 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -43,7 +41,7 @@ export type ResetPasswordFormProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps) => {
|
export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps) => {
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@ -58,15 +56,15 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps)
|
|||||||
|
|
||||||
const isSubmitting = form.formState.isSubmitting;
|
const isSubmitting = form.formState.isSubmitting;
|
||||||
|
|
||||||
const { mutateAsync: resetPassword } = trpc.profile.resetPassword.useMutation();
|
|
||||||
|
|
||||||
const onFormSubmit = async ({ password }: Omit<TResetPasswordFormSchema, 'repeatedPassword'>) => {
|
const onFormSubmit = async ({ password }: Omit<TResetPasswordFormSchema, 'repeatedPassword'>) => {
|
||||||
try {
|
try {
|
||||||
await resetPassword({
|
await authClient.emailPassword.resetPassword({
|
||||||
password,
|
password,
|
||||||
token,
|
token,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await navigate('/signin');
|
||||||
|
|
||||||
form.reset();
|
form.reset();
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@ -74,8 +72,6 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps)
|
|||||||
description: _(msg`Your password has been updated successfully.`),
|
description: _(msg`Your password has been updated successfully.`),
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push('/signin');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
import { useLocation, useNavigate } from 'react-router';
|
||||||
|
import { useSearchParams } from 'react-router';
|
||||||
|
|
||||||
import { Select, SelectContent, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
|
import { Select, SelectContent, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
|
||||||
|
|
||||||
@ -11,10 +12,10 @@ export type SearchParamSelector = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SearchParamSelector = ({ children, paramKey, isValueValid }: SearchParamSelector) => {
|
export const SearchParamSelector = ({ children, paramKey, isValueValid }: SearchParamSelector) => {
|
||||||
const pathname = usePathname();
|
const { pathname } = useLocation();
|
||||||
const searchParams = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const value = useMemo(() => {
|
const value = useMemo(() => {
|
||||||
const p = searchParams?.get(paramKey) ?? 'all';
|
const p = searchParams?.get(paramKey) ?? 'all';
|
||||||
@ -35,7 +36,7 @@ export const SearchParamSelector = ({ children, paramKey, isValueValid }: Search
|
|||||||
params.delete(paramKey);
|
params.delete(paramKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
router.push(`${pathname}?${params.toString()}`, { scroll: false });
|
void navigate(`${pathname}?${params.toString()}`, { preventScrollReset: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -1,12 +1,11 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -43,11 +42,9 @@ export const SendConfirmationEmailForm = ({ className }: SendConfirmationEmailFo
|
|||||||
|
|
||||||
const isSubmitting = form.formState.isSubmitting;
|
const isSubmitting = form.formState.isSubmitting;
|
||||||
|
|
||||||
const { mutateAsync: sendConfirmationEmail } = trpc.profile.sendConfirmationEmail.useMutation();
|
|
||||||
|
|
||||||
const onFormSubmit = async ({ email }: TSendConfirmationEmailFormSchema) => {
|
const onFormSubmit = async ({ email }: TSendConfirmationEmailFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await sendConfirmationEmail({ email });
|
await authClient.emailPassword.resendVerifyEmail({ email });
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Confirmation email sent`),
|
title: _(msg`Confirmation email sent`),
|
||||||
@ -60,6 +57,7 @@ export const SendConfirmationEmailForm = ({ className }: SendConfirmationEmailFo
|
|||||||
form.reset();
|
form.reset();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
|
variant: 'destructive',
|
||||||
title: _(msg`An error occurred while sending your confirmation email`),
|
title: _(msg`An error occurred while sending your confirmation email`),
|
||||||
description: _(msg`Please try again and make sure you enter the correct email address.`),
|
description: _(msg`Please try again and make sure you enter the correct email address.`),
|
||||||
});
|
});
|
||||||
@ -1,25 +1,22 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import type { MessageDescriptor } from '@lingui/core';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser';
|
import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser';
|
||||||
import { KeyRoundIcon } from 'lucide-react';
|
import { KeyRoundIcon } from 'lucide-react';
|
||||||
import { signIn } from 'next-auth/react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { FaIdCardClip } from 'react-icons/fa6';
|
import { FaIdCardClip } from 'react-icons/fa6';
|
||||||
import { FcGoogle } from 'react-icons/fc';
|
import { FcGoogle } from 'react-icons/fc';
|
||||||
|
import { Link, useNavigate } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AuthenticationErrorCode } from '@documenso/auth/server/lib/errors/error-codes';
|
||||||
import { ErrorCode, isErrorCode } from '@documenso/lib/next-auth/error-codes';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { ZCurrentPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
import { ZCurrentPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@ -44,19 +41,19 @@ import { PasswordInput } from '@documenso/ui/primitives/password-input';
|
|||||||
import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input';
|
import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
const ERROR_MESSAGES: Partial<Record<keyof typeof ErrorCode, string>> = {
|
const CommonErrorMessages: Record<string, MessageDescriptor> = {
|
||||||
[ErrorCode.CREDENTIALS_NOT_FOUND]: 'The email or password provided is incorrect',
|
[AuthenticationErrorCode.AccountDisabled]: msg`This account has been disabled. Please contact support.`,
|
||||||
[ErrorCode.INCORRECT_EMAIL_PASSWORD]: 'The email or password provided is incorrect',
|
|
||||||
[ErrorCode.USER_MISSING_PASSWORD]:
|
|
||||||
'This account appears to be using a social login method, please sign in using that method',
|
|
||||||
[ErrorCode.INCORRECT_TWO_FACTOR_CODE]: 'The two-factor authentication code provided is incorrect',
|
|
||||||
[ErrorCode.INCORRECT_TWO_FACTOR_BACKUP_CODE]: 'The backup code provided is incorrect',
|
|
||||||
[ErrorCode.UNVERIFIED_EMAIL]:
|
|
||||||
'This account has not been verified. Please verify your account before signing in.',
|
|
||||||
[ErrorCode.ACCOUNT_DISABLED]: 'This account has been disabled. Please contact support.',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const TwoFactorEnabledErrorCode = ErrorCode.TWO_FACTOR_MISSING_CREDENTIALS;
|
const handleFallbackErrorMessages = (code: string) => {
|
||||||
|
const message = CommonErrorMessages[code];
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return msg`An unknown error occurred`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
};
|
||||||
|
|
||||||
const LOGIN_REDIRECT_PATH = '/documents';
|
const LOGIN_REDIRECT_PATH = '/documents';
|
||||||
|
|
||||||
@ -88,9 +85,8 @@ export const SignInForm = ({
|
|||||||
}: SignInFormProps) => {
|
}: SignInFormProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { getFlag } = useFeatureFlags();
|
|
||||||
|
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] =
|
const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
@ -101,9 +97,7 @@ export const SignInForm = ({
|
|||||||
|
|
||||||
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
|
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
|
||||||
|
|
||||||
const isPasskeyEnabled = getFlag('app_passkey');
|
const redirectPath = useMemo(() => {
|
||||||
|
|
||||||
const callbackUrl = useMemo(() => {
|
|
||||||
// Handle SSR
|
// Handle SSR
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return LOGIN_REDIRECT_PATH;
|
return LOGIN_REDIRECT_PATH;
|
||||||
@ -170,25 +164,20 @@ export const SignInForm = ({
|
|||||||
try {
|
try {
|
||||||
setIsPasskeyLoading(true);
|
setIsPasskeyLoading(true);
|
||||||
|
|
||||||
const options = await createPasskeySigninOptions();
|
const { options, sessionId } = await createPasskeySigninOptions();
|
||||||
|
|
||||||
const credential = await startAuthentication(options);
|
const credential = await startAuthentication(options);
|
||||||
|
|
||||||
const result = await signIn('webauthn', {
|
await authClient.passkey.signIn({
|
||||||
credential: JSON.stringify(credential),
|
credential: JSON.stringify(credential),
|
||||||
callbackUrl,
|
csrfToken: sessionId,
|
||||||
redirect: false,
|
redirectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result?.url || result.error) {
|
|
||||||
throw new AppError(result?.error ?? '');
|
|
||||||
}
|
|
||||||
|
|
||||||
window.location.href = result.url;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setIsPasskeyLoading(false);
|
setIsPasskeyLoading(false);
|
||||||
|
|
||||||
if (err.name === 'NotAllowedError') {
|
// Error from library.
|
||||||
|
if (err instanceof Error && err.name === 'NotAllowedError') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,12 +185,15 @@ export const SignInForm = ({
|
|||||||
|
|
||||||
const errorMessage = match(error.code)
|
const errorMessage = match(error.code)
|
||||||
.with(
|
.with(
|
||||||
AppErrorCode.NOT_SETUP,
|
AuthenticationErrorCode.NotSetup,
|
||||||
() =>
|
() =>
|
||||||
msg`This passkey is not configured for this application. Please login and add one in the user settings.`,
|
msg`This passkey is not configured for this application. Please login and add one in the user settings.`,
|
||||||
)
|
)
|
||||||
.with(AppErrorCode.EXPIRED_CODE, () => msg`This session has expired. Please try again.`)
|
.with(
|
||||||
.otherwise(() => msg`Please try again later or login using your normal details`);
|
AuthenticationErrorCode.SessionExpired,
|
||||||
|
() => msg`This session has expired. Please try again.`,
|
||||||
|
)
|
||||||
|
.otherwise(() => handleFallbackErrorMessages(error.code));
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: 'Something went wrong',
|
title: 'Something went wrong',
|
||||||
@ -214,73 +206,58 @@ export const SignInForm = ({
|
|||||||
|
|
||||||
const onFormSubmit = async ({ email, password, totpCode, backupCode }: TSignInFormSchema) => {
|
const onFormSubmit = async ({ email, password, totpCode, backupCode }: TSignInFormSchema) => {
|
||||||
try {
|
try {
|
||||||
const credentials: Record<string, string> = {
|
await authClient.emailPassword.signIn({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
};
|
totpCode,
|
||||||
|
backupCode,
|
||||||
if (totpCode) {
|
redirectPath,
|
||||||
credentials.totpCode = totpCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (backupCode) {
|
|
||||||
credentials.backupCode = backupCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await signIn('credentials', {
|
|
||||||
...credentials,
|
|
||||||
callbackUrl,
|
|
||||||
redirect: false,
|
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
|
||||||
if (result?.error && isErrorCode(result.error)) {
|
const error = AppError.parseError(err);
|
||||||
if (result.error === TwoFactorEnabledErrorCode) {
|
|
||||||
|
if (error.code === 'TWO_FACTOR_MISSING_CREDENTIALS') {
|
||||||
setIsTwoFactorAuthenticationDialogOpen(true);
|
setIsTwoFactorAuthenticationDialogOpen(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorMessage = ERROR_MESSAGES[result.error];
|
if (error.code === AuthenticationErrorCode.UnverifiedEmail) {
|
||||||
|
await navigate('/unverified-account');
|
||||||
if (result.error === ErrorCode.UNVERIFIED_EMAIL) {
|
|
||||||
router.push(`/unverified-account`);
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Unable to sign in`),
|
title: _(msg`Unable to sign in`),
|
||||||
description: errorMessage ?? _(msg`An unknown error occurred`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: _(msg`Unable to sign in`),
|
|
||||||
description: errorMessage ?? _(msg`An unknown error occurred`),
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result?.url) {
|
|
||||||
throw new Error('An unknown error occurred');
|
|
||||||
}
|
|
||||||
|
|
||||||
window.location.href = result.url;
|
|
||||||
} catch (err) {
|
|
||||||
toast({
|
|
||||||
title: _(msg`An unknown error occurred`),
|
|
||||||
description: _(
|
description: _(
|
||||||
msg`We encountered an unknown error while attempting to sign you In. Please try again later.`,
|
msg`This account has not been verified. Please verify your account before signing in.`,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessage = match(error.code)
|
||||||
|
.with(
|
||||||
|
AuthenticationErrorCode.InvalidCredentials,
|
||||||
|
() => msg`The email or password provided is incorrect`,
|
||||||
|
)
|
||||||
|
.with(
|
||||||
|
AuthenticationErrorCode.InvalidTwoFactorCode,
|
||||||
|
() => msg`The two-factor authentication code provided is incorrect`,
|
||||||
|
)
|
||||||
|
.otherwise(() => handleFallbackErrorMessages(error.code));
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: _(msg`Unable to sign in`),
|
||||||
|
description: _(errorMessage),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSignInWithGoogleClick = async () => {
|
const onSignInWithGoogleClick = async () => {
|
||||||
try {
|
try {
|
||||||
await signIn('google', {
|
await authClient.google.signIn();
|
||||||
callbackUrl,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
@ -294,9 +271,11 @@ export const SignInForm = ({
|
|||||||
|
|
||||||
const onSignInWithOIDCClick = async () => {
|
const onSignInWithOIDCClick = async () => {
|
||||||
try {
|
try {
|
||||||
await signIn('oidc', {
|
// eslint-disable-next-line no-promise-executor-return
|
||||||
callbackUrl,
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
});
|
// await signIn('oidc', {
|
||||||
|
// callbackUrl,
|
||||||
|
// });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
@ -365,7 +344,7 @@ export const SignInForm = ({
|
|||||||
|
|
||||||
<p className="mt-2 text-right">
|
<p className="mt-2 text-right">
|
||||||
<Link
|
<Link
|
||||||
href="/forgot-password"
|
to="/forgot-password"
|
||||||
className="text-muted-foreground text-sm duration-200 hover:opacity-70"
|
className="text-muted-foreground text-sm duration-200 hover:opacity-70"
|
||||||
>
|
>
|
||||||
<Trans>Forgot your password?</Trans>
|
<Trans>Forgot your password?</Trans>
|
||||||
@ -384,7 +363,7 @@ export const SignInForm = ({
|
|||||||
{isSubmitting ? <Trans>Signing in...</Trans> : <Trans>Sign In</Trans>}
|
{isSubmitting ? <Trans>Signing in...</Trans> : <Trans>Sign In</Trans>}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{(isGoogleSSOEnabled || isPasskeyEnabled || isOIDCSSOEnabled) && (
|
{(isGoogleSSOEnabled || isOIDCSSOEnabled) && (
|
||||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||||
<div className="bg-border h-px flex-1" />
|
<div className="bg-border h-px flex-1" />
|
||||||
<span className="text-muted-foreground bg-transparent">
|
<span className="text-muted-foreground bg-transparent">
|
||||||
@ -422,7 +401,6 @@ export const SignInForm = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isPasskeyEnabled && (
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="lg"
|
size="lg"
|
||||||
@ -435,7 +413,6 @@ export const SignInForm = ({
|
|||||||
{!isPasskeyLoading && <KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />}
|
{!isPasskeyLoading && <KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />}
|
||||||
<Trans>Passkey</Trans>
|
<Trans>Passkey</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -1,27 +1,22 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import type { MessageDescriptor } from '@lingui/core';
|
import type { MessageDescriptor } from '@lingui/core';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { signIn } from 'next-auth/react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { FaIdCardClip } from 'react-icons/fa6';
|
import { FaIdCardClip } from 'react-icons/fa6';
|
||||||
import { FcGoogle } from 'react-icons/fc';
|
import { FcGoogle } from 'react-icons/fc';
|
||||||
|
import { Link, useNavigate, useSearchParams } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import communityCardsImage from '@documenso/assets/images/community-cards.png';
|
import communityCardsImage from '@documenso/assets/images/community-cards.png';
|
||||||
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -38,14 +33,14 @@ import { PasswordInput } from '@documenso/ui/primitives/password-input';
|
|||||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { UserProfileSkeleton } from '~/components/ui/user-profile-skeleton';
|
import { UserProfileSkeleton } from '~/components/general/user-profile-skeleton';
|
||||||
import { UserProfileTimur } from '~/components/ui/user-profile-timur';
|
import { UserProfileTimur } from '~/components/general/user-profile-timur';
|
||||||
|
|
||||||
const SIGN_UP_REDIRECT_PATH = '/documents';
|
const SIGN_UP_REDIRECT_PATH = '/documents';
|
||||||
|
|
||||||
type SignUpStep = 'BASIC_DETAILS' | 'CLAIM_USERNAME';
|
type SignUpStep = 'BASIC_DETAILS' | 'CLAIM_USERNAME';
|
||||||
|
|
||||||
export const ZSignUpFormV2Schema = z
|
export const ZSignUpFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
@ -78,39 +73,39 @@ export const signupErrorMessages: Record<string, MessageDescriptor> = {
|
|||||||
SIGNUP_DISABLED: msg`Signups are disabled.`,
|
SIGNUP_DISABLED: msg`Signups are disabled.`,
|
||||||
[AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`,
|
[AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`,
|
||||||
[AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`,
|
[AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`,
|
||||||
[AppErrorCode.PROFILE_URL_TAKEN]: msg`This username has already been taken`,
|
PROFILE_URL_TAKEN: msg`This username has already been taken`,
|
||||||
[AppErrorCode.PREMIUM_PROFILE_URL]: msg`Only subscribers can have a username shorter than 6 characters`,
|
PREMIUM_PROFILE_URL: msg`Only subscribers can have a username shorter than 6 characters`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSignUpFormV2Schema = z.infer<typeof ZSignUpFormV2Schema>;
|
export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
|
||||||
|
|
||||||
export type SignUpFormV2Props = {
|
export type SignUpFormProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
initialEmail?: string;
|
initialEmail?: string;
|
||||||
isGoogleSSOEnabled?: boolean;
|
isGoogleSSOEnabled?: boolean;
|
||||||
isOIDCSSOEnabled?: boolean;
|
isOIDCSSOEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SignUpFormV2 = ({
|
export const SignUpForm = ({
|
||||||
className,
|
className,
|
||||||
initialEmail,
|
initialEmail,
|
||||||
isGoogleSSOEnabled,
|
isGoogleSSOEnabled,
|
||||||
isOIDCSSOEnabled,
|
isOIDCSSOEnabled,
|
||||||
}: SignUpFormV2Props) => {
|
}: SignUpFormProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const analytics = useAnalytics();
|
const analytics = useAnalytics();
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const searchParams = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const [step, setStep] = useState<SignUpStep>('BASIC_DETAILS');
|
const [step, setStep] = useState<SignUpStep>('BASIC_DETAILS');
|
||||||
|
|
||||||
const utmSrc = searchParams?.get('utm_source') ?? null;
|
const utmSrc = searchParams.get('utm_source') ?? null;
|
||||||
|
|
||||||
const baseUrl = new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000');
|
const baseUrl = new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000');
|
||||||
|
|
||||||
const form = useForm<TSignUpFormV2Schema>({
|
const form = useForm<TSignUpFormSchema>({
|
||||||
values: {
|
values: {
|
||||||
name: '',
|
name: '',
|
||||||
email: initialEmail ?? '',
|
email: initialEmail ?? '',
|
||||||
@ -119,7 +114,7 @@ export const SignUpFormV2 = ({
|
|||||||
url: '',
|
url: '',
|
||||||
},
|
},
|
||||||
mode: 'onBlur',
|
mode: 'onBlur',
|
||||||
resolver: zodResolver(ZSignUpFormV2Schema),
|
resolver: zodResolver(ZSignUpFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const isSubmitting = form.formState.isSubmitting;
|
const isSubmitting = form.formState.isSubmitting;
|
||||||
@ -127,13 +122,17 @@ export const SignUpFormV2 = ({
|
|||||||
const name = form.watch('name');
|
const name = form.watch('name');
|
||||||
const url = form.watch('url');
|
const url = form.watch('url');
|
||||||
|
|
||||||
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
|
const onFormSubmit = async ({ name, email, password, signature, url }: TSignUpFormSchema) => {
|
||||||
|
|
||||||
const onFormSubmit = async ({ name, email, password, signature, url }: TSignUpFormV2Schema) => {
|
|
||||||
try {
|
try {
|
||||||
await signup({ name, email, password, signature, url });
|
await authClient.emailPassword.signUp({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
signature,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
|
||||||
router.push(`/unverified-account`);
|
await navigate(`/unverified-account`);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Registration Successful`),
|
title: _(msg`Registration Successful`),
|
||||||
@ -153,10 +152,7 @@ export const SignUpFormV2 = ({
|
|||||||
|
|
||||||
const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST;
|
const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST;
|
||||||
|
|
||||||
if (
|
if (error.code === 'PROFILE_URL_TAKEN' || error.code === 'PREMIUM_PROFILE_URL') {
|
||||||
error.code === AppErrorCode.PROFILE_URL_TAKEN ||
|
|
||||||
error.code === AppErrorCode.PREMIUM_PROFILE_URL
|
|
||||||
) {
|
|
||||||
form.setError('url', {
|
form.setError('url', {
|
||||||
type: 'manual',
|
type: 'manual',
|
||||||
message: _(errorMessage),
|
message: _(errorMessage),
|
||||||
@ -181,7 +177,7 @@ export const SignUpFormV2 = ({
|
|||||||
|
|
||||||
const onSignUpWithGoogleClick = async () => {
|
const onSignUpWithGoogleClick = async () => {
|
||||||
try {
|
try {
|
||||||
await signIn('google', { callbackUrl: SIGN_UP_REDIRECT_PATH });
|
await authClient.google.signIn();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
@ -195,7 +191,9 @@ export const SignUpFormV2 = ({
|
|||||||
|
|
||||||
const onSignUpWithOIDCClick = async () => {
|
const onSignUpWithOIDCClick = async () => {
|
||||||
try {
|
try {
|
||||||
await signIn('oidc', { callbackUrl: SIGN_UP_REDIRECT_PATH });
|
// eslint-disable-next-line no-promise-executor-return
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
// await signIn('oidc', { callbackUrl: SIGN_UP_REDIRECT_PATH });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
@ -223,11 +221,10 @@ export const SignUpFormV2 = ({
|
|||||||
<div className={cn('flex justify-center gap-x-12', className)}>
|
<div className={cn('flex justify-center gap-x-12', className)}>
|
||||||
<div className="border-border relative hidden flex-1 overflow-hidden rounded-xl border xl:flex">
|
<div className="border-border relative hidden flex-1 overflow-hidden rounded-xl border xl:flex">
|
||||||
<div className="absolute -inset-8 -z-[2] backdrop-blur">
|
<div className="absolute -inset-8 -z-[2] backdrop-blur">
|
||||||
<Image
|
<img
|
||||||
src={communityCardsImage}
|
src={communityCardsImage}
|
||||||
fill={true}
|
|
||||||
alt="community-cards"
|
alt="community-cards"
|
||||||
className="dark:brightness-95 dark:contrast-[70%] dark:invert"
|
className="h-full w-full object-cover dark:brightness-95 dark:contrast-[70%] dark:invert"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -426,10 +423,7 @@ export const SignUpFormV2 = ({
|
|||||||
<p className="text-muted-foreground mt-4 text-sm">
|
<p className="text-muted-foreground mt-4 text-sm">
|
||||||
<Trans>
|
<Trans>
|
||||||
Already have an account?{' '}
|
Already have an account?{' '}
|
||||||
<Link
|
<Link to="/signin" className="text-documenso-700 duration-200 hover:opacity-70">
|
||||||
href="/signin"
|
|
||||||
className="text-documenso-700 duration-200 hover:opacity-70"
|
|
||||||
>
|
|
||||||
Sign in instead
|
Sign in instead
|
||||||
</Link>
|
</Link>
|
||||||
</Trans>
|
</Trans>
|
||||||
@ -1,17 +1,16 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Team, TeamGlobalSettings } from '@prisma/client';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
||||||
import { putFile } from '@documenso/lib/universal/upload/put-file';
|
import { putFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
import type { Team, TeamGlobalSettings } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -1,19 +1,18 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Team, TeamGlobalSettings } from '@prisma/client';
|
||||||
|
import { DocumentVisibility } from '@prisma/client';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import {
|
import {
|
||||||
SUPPORTED_LANGUAGES,
|
SUPPORTED_LANGUAGES,
|
||||||
SUPPORTED_LANGUAGE_CODES,
|
SUPPORTED_LANGUAGE_CODES,
|
||||||
isValidLanguageCode,
|
isValidLanguageCode,
|
||||||
} from '@documenso/lib/constants/i18n';
|
} from '@documenso/lib/constants/i18n';
|
||||||
import type { Team, TeamGlobalSettings } from '@documenso/prisma/client';
|
|
||||||
import { DocumentVisibility } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert } from '@documenso/ui/primitives/alert';
|
import { Alert } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -56,9 +55,9 @@ export const TeamDocumentPreferencesForm = ({
|
|||||||
}: TeamDocumentPreferencesFormProps) => {
|
}: TeamDocumentPreferencesFormProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { data } = useSession();
|
const { user } = useSession();
|
||||||
|
|
||||||
const placeholderEmail = data?.user.email ?? 'user@example.com';
|
const placeholderEmail = user.email ?? 'user@example.com';
|
||||||
|
|
||||||
const { mutateAsync: updateTeamDocumentPreferences } =
|
const { mutateAsync: updateTeamDocumentPreferences } =
|
||||||
trpc.team.updateTeamDocumentSettings.useMutation();
|
trpc.team.updateTeamDocumentSettings.useMutation();
|
||||||
@ -1,15 +1,13 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { ZUpdateTeamMutationSchema } from '@documenso/trpc/server/team-router/schema';
|
import { ZUpdateTeamMutationSchema } from '@documenso/trpc/server/team-router/schema';
|
||||||
@ -31,20 +29,20 @@ export type UpdateTeamDialogProps = {
|
|||||||
teamUrl: string;
|
teamUrl: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ZUpdateTeamFormSchema = ZUpdateTeamMutationSchema.shape.data.pick({
|
const ZTeamUpdateFormSchema = ZUpdateTeamMutationSchema.shape.data.pick({
|
||||||
name: true,
|
name: true,
|
||||||
url: true,
|
url: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
type TUpdateTeamFormSchema = z.infer<typeof ZUpdateTeamFormSchema>;
|
type TTeamUpdateFormSchema = z.infer<typeof ZTeamUpdateFormSchema>;
|
||||||
|
|
||||||
export const UpdateTeamForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogProps) => {
|
export const TeamUpdateForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogProps) => {
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(ZUpdateTeamFormSchema),
|
resolver: zodResolver(ZTeamUpdateFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: teamName,
|
name: teamName,
|
||||||
url: teamUrl,
|
url: teamUrl,
|
||||||
@ -53,7 +51,7 @@ export const UpdateTeamForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogPr
|
|||||||
|
|
||||||
const { mutateAsync: updateTeam } = trpc.team.updateTeam.useMutation();
|
const { mutateAsync: updateTeam } = trpc.team.updateTeam.useMutation();
|
||||||
|
|
||||||
const onFormSubmit = async ({ name, url }: TUpdateTeamFormSchema) => {
|
const onFormSubmit = async ({ name, url }: TTeamUpdateFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await updateTeam({
|
await updateTeam({
|
||||||
data: {
|
data: {
|
||||||
@ -75,7 +73,7 @@ export const UpdateTeamForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogPr
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (url !== teamUrl) {
|
if (url !== teamUrl) {
|
||||||
router.push(`${WEBAPP_BASE_URL}/t/${url}/settings`);
|
await navigate(`/t/${url}/settings`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
@ -133,7 +131,7 @@ export const UpdateTeamForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogPr
|
|||||||
{!form.formState.errors.url && (
|
{!form.formState.errors.url && (
|
||||||
<span className="text-foreground/50 text-xs font-normal">
|
<span className="text-foreground/50 text-xs font-normal">
|
||||||
{field.value ? (
|
{field.value ? (
|
||||||
`${WEBAPP_BASE_URL}/t/${field.value}`
|
`${NEXT_PUBLIC_WEBAPP_URL()}/t/${field.value}`
|
||||||
) : (
|
) : (
|
||||||
<Trans>A unique URL to identify your team</Trans>
|
<Trans>A unique URL to identify your team</Trans>
|
||||||
)}
|
)}
|
||||||
@ -1,12 +1,10 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useTransition } from 'react';
|
import { useState, useTransition } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { ApiToken } from '@prisma/client';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
@ -14,7 +12,6 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { ApiToken } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type { TCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
|
import type { TCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
|
||||||
import { ZCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
|
import { ZCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
|
||||||
@ -41,7 +38,13 @@ import {
|
|||||||
import { Switch } from '@documenso/ui/primitives/switch';
|
import { Switch } from '@documenso/ui/primitives/switch';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { EXPIRATION_DATES } from '../(dashboard)/settings/token/contants';
|
export const EXPIRATION_DATES = {
|
||||||
|
ONE_WEEK: msg`7 days`,
|
||||||
|
ONE_MONTH: msg`1 month`,
|
||||||
|
THREE_MONTHS: msg`3 months`,
|
||||||
|
SIX_MONTHS: msg`6 months`,
|
||||||
|
ONE_YEAR: msg`12 months`,
|
||||||
|
} as const;
|
||||||
|
|
||||||
const ZCreateTokenFormSchema = ZCreateTokenMutationSchema.extend({
|
const ZCreateTokenFormSchema = ZCreateTokenMutationSchema.extend({
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
@ -61,7 +64,6 @@ export type ApiTokenFormProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) => {
|
export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) => {
|
||||||
const router = useRouter();
|
|
||||||
const [isTransitionPending, startTransition] = useTransition();
|
const [isTransitionPending, startTransition] = useTransition();
|
||||||
|
|
||||||
const [, copy] = useCopyToClipboard();
|
const [, copy] = useCopyToClipboard();
|
||||||
@ -72,13 +74,6 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
|
|||||||
const [newlyCreatedToken, setNewlyCreatedToken] = useState<NewlyCreatedToken | null>();
|
const [newlyCreatedToken, setNewlyCreatedToken] = useState<NewlyCreatedToken | null>();
|
||||||
const [noExpirationDate, setNoExpirationDate] = useState(false);
|
const [noExpirationDate, setNoExpirationDate] = useState(false);
|
||||||
|
|
||||||
// This lets us hide the token from being copied if it has been deleted without
|
|
||||||
// resorting to a useEffect or any other fanciness. This comes at the cost of it
|
|
||||||
// taking slighly longer to appear since it will need to wait for the router.refresh()
|
|
||||||
// to finish updating.
|
|
||||||
const hasNewlyCreatedToken =
|
|
||||||
tokens?.find((token) => token.id === newlyCreatedToken?.id) !== undefined;
|
|
||||||
|
|
||||||
const { mutateAsync: createTokenMutation } = trpc.apiToken.createToken.useMutation({
|
const { mutateAsync: createTokenMutation } = trpc.apiToken.createToken.useMutation({
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
setNewlyCreatedToken(data);
|
setNewlyCreatedToken(data);
|
||||||
@ -130,8 +125,6 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
|
|||||||
});
|
});
|
||||||
|
|
||||||
form.reset();
|
form.reset();
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -263,8 +256,10 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
|
|||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<AnimatePresence initial={!hasNewlyCreatedToken}>
|
<AnimatePresence>
|
||||||
{newlyCreatedToken && hasNewlyCreatedToken && (
|
{newlyCreatedToken &&
|
||||||
|
tokens &&
|
||||||
|
tokens.find((token) => token.id === newlyCreatedToken.id) && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="mt-8"
|
className="mt-8"
|
||||||
initial={{ opacity: 0, y: -40 }}
|
initial={{ opacity: 0, y: -40 }}
|
||||||
@ -1,23 +1,21 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||||
|
|
||||||
import type { GetSignerConversionMonthlyResult } from '@documenso/lib/server-only/user/get-signer-conversion';
|
import type { GetSignerConversionMonthlyResult } from '@documenso/lib/server-only/user/get-signer-conversion';
|
||||||
|
|
||||||
export type SignerConversionChartProps = {
|
export type AdminStatsSignerConversionChartProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
title: string;
|
title: string;
|
||||||
cummulative?: boolean;
|
cummulative?: boolean;
|
||||||
data: GetSignerConversionMonthlyResult;
|
data: GetSignerConversionMonthlyResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SignerConversionChart = ({
|
export const AdminStatsSignerConversionChart = ({
|
||||||
className,
|
className,
|
||||||
data,
|
data,
|
||||||
title,
|
title,
|
||||||
cummulative = false,
|
cummulative = false,
|
||||||
}: SignerConversionChartProps) => {
|
}: AdminStatsSignerConversionChartProps) => {
|
||||||
const formattedData = [...data].reverse().map(({ month, count, cume_count }) => {
|
const formattedData = [...data].reverse().map(({ month, count, cume_count }) => {
|
||||||
return {
|
return {
|
||||||
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('MMM yyyy'),
|
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('MMM yyyy'),
|
||||||
@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||||
import type { TooltipProps } from 'recharts';
|
import type { TooltipProps } from 'recharts';
|
||||||
@ -7,7 +5,7 @@ import type { NameType, ValueType } from 'recharts/types/component/DefaultToolti
|
|||||||
|
|
||||||
import type { GetUserWithDocumentMonthlyGrowth } from '@documenso/lib/server-only/admin/get-users-stats';
|
import type { GetUserWithDocumentMonthlyGrowth } from '@documenso/lib/server-only/admin/get-users-stats';
|
||||||
|
|
||||||
export type UserWithDocumentChartProps = {
|
export type AdminStatsUsersWithDocumentsChartProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
title: string;
|
title: string;
|
||||||
data: GetUserWithDocumentMonthlyGrowth;
|
data: GetUserWithDocumentMonthlyGrowth;
|
||||||
@ -36,13 +34,13 @@ const CustomTooltip = ({
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserWithDocumentChart = ({
|
export const AdminStatsUsersWithDocumentsChart = ({
|
||||||
className,
|
className,
|
||||||
data,
|
data,
|
||||||
title,
|
title,
|
||||||
completed = false,
|
completed = false,
|
||||||
tooltip,
|
tooltip,
|
||||||
}: UserWithDocumentChartProps) => {
|
}: AdminStatsUsersWithDocumentsChartProps) => {
|
||||||
const formattedData = (data: GetUserWithDocumentMonthlyGrowth, completed: boolean) => {
|
const formattedData = (data: GetUserWithDocumentMonthlyGrowth, completed: boolean) => {
|
||||||
return [...data].reverse().map(({ month, count, signed_count }) => {
|
return [...data].reverse().map(({ month, count, signed_count }) => {
|
||||||
const formattedMonth = DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL');
|
const formattedMonth = DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL');
|
||||||
28
apps/remix/app/components/general/app-banner.tsx
Normal file
28
apps/remix/app/components/general/app-banner.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { type TSiteSettingsBannerSchema } from '@documenso/lib/server-only/site-settings/schemas/banner';
|
||||||
|
|
||||||
|
export type AppBannerProps = {
|
||||||
|
banner: TSiteSettingsBannerSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppBanner = ({ banner }: AppBannerProps) => {
|
||||||
|
if (!banner.enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-2" style={{ background: banner.data.bgColor }}>
|
||||||
|
<div
|
||||||
|
className="mx-auto flex h-auto max-w-screen-xl items-center justify-center px-4 py-3 text-sm font-medium"
|
||||||
|
style={{ color: banner.data.textColor }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span dangerouslySetInnerHTML={{ __html: banner.data.content }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Banner
|
||||||
|
// Custom Text
|
||||||
|
// Custom Text with Custom Icon
|
||||||
@ -1,15 +1,13 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import type { MessageDescriptor } from '@lingui/core';
|
import type { MessageDescriptor } from '@lingui/core';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { CheckIcon, Loader, Monitor, Moon, Sun } from 'lucide-react';
|
import { CheckIcon, Loader, Monitor, Moon, Sun } from 'lucide-react';
|
||||||
import { useTheme } from 'next-themes';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
import { Theme, useTheme } from 'remix-themes';
|
||||||
|
|
||||||
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
||||||
import {
|
import {
|
||||||
@ -21,7 +19,6 @@ import {
|
|||||||
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
SKIP_QUERY_BATCH_META,
|
SKIP_QUERY_BATCH_META,
|
||||||
} from '@documenso/lib/constants/trpc';
|
} from '@documenso/lib/constants/trpc';
|
||||||
import { switchI18NLanguage } from '@documenso/lib/server-only/i18n/switch-i18n-language';
|
|
||||||
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@ -34,7 +31,6 @@ import {
|
|||||||
CommandList,
|
CommandList,
|
||||||
CommandShortcut,
|
CommandShortcut,
|
||||||
} from '@documenso/ui/primitives/command';
|
} from '@documenso/ui/primitives/command';
|
||||||
import { THEMES_TYPE } from '@documenso/ui/primitives/constants';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
const DOCUMENTS_PAGES = [
|
const DOCUMENTS_PAGES = [
|
||||||
@ -70,22 +66,21 @@ const SETTINGS_PAGES = [
|
|||||||
{ label: msg`Password`, path: '/settings/password' },
|
{ label: msg`Password`, path: '/settings/password' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export type CommandMenuProps = {
|
export type AppCommandMenuProps = {
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
onOpenChange?: (_open: boolean) => void;
|
onOpenChange?: (_open: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
export function AppCommandMenu({ open, onOpenChange }: AppCommandMenuProps) {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { setTheme } = useTheme();
|
|
||||||
|
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(() => open ?? false);
|
const [isOpen, setIsOpen] = useState(() => open ?? false);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [pages, setPages] = useState<string[]>([]);
|
const [pages, setPages] = useState<string[]>([]);
|
||||||
|
|
||||||
const { data: searchDocumentsData, isPending: isSearchingDocuments } =
|
const { data: searchDocumentsData, isLoading: isSearchingDocuments } =
|
||||||
trpcReact.document.searchDocuments.useQuery(
|
trpcReact.document.searchDocuments.useQuery(
|
||||||
{
|
{
|
||||||
query: search,
|
query: search,
|
||||||
@ -138,10 +133,10 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
|||||||
|
|
||||||
const push = useCallback(
|
const push = useCallback(
|
||||||
(path: string) => {
|
(path: string) => {
|
||||||
router.push(path);
|
void navigate(path);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
},
|
},
|
||||||
[router, setOpen],
|
[setOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
const addPage = (page: string) => {
|
const addPage = (page: string) => {
|
||||||
@ -227,7 +222,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentPage === 'theme' && <ThemeCommands setTheme={setTheme} />}
|
{currentPage === 'theme' && <ThemeCommands />}
|
||||||
{currentPage === 'language' && <LanguageCommands />}
|
{currentPage === 'language' && <LanguageCommands />}
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandDialog>
|
</CommandDialog>
|
||||||
@ -256,19 +251,18 @@ const Commands = ({
|
|||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
const ThemeCommands = ({ setTheme }: { setTheme: (_theme: string) => void }) => {
|
const ThemeCommands = () => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const THEMES = useMemo(
|
const [, setTheme] = useTheme();
|
||||||
() => [
|
|
||||||
{ label: msg`Light Mode`, theme: THEMES_TYPE.LIGHT, icon: Sun },
|
|
||||||
{ label: msg`Dark Mode`, theme: THEMES_TYPE.DARK, icon: Moon },
|
|
||||||
{ label: msg`System Theme`, theme: THEMES_TYPE.SYSTEM, icon: Monitor },
|
|
||||||
],
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
return THEMES.map((theme) => (
|
const themes = [
|
||||||
|
{ label: msg`Light Mode`, theme: Theme.LIGHT, icon: Sun },
|
||||||
|
{ label: msg`Dark Mode`, theme: Theme.DARK, icon: Moon },
|
||||||
|
{ label: msg`System Theme`, theme: null, icon: Monitor },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
return themes.map((theme) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={theme.theme}
|
key={theme.theme}
|
||||||
onSelect={() => setTheme(theme.theme)}
|
onSelect={() => setTheme(theme.theme)}
|
||||||
@ -294,9 +288,23 @@ const LanguageCommands = () => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await dynamicActivate(i18n, lang);
|
await dynamicActivate(lang);
|
||||||
await switchI18NLanguage(lang);
|
|
||||||
} catch (err) {
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('lang', lang);
|
||||||
|
|
||||||
|
const response = await fetch('/api/locale', {
|
||||||
|
method: 'post',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to set language: ${e}`);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
@ -1,32 +1,28 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { type HTMLAttributes, useEffect, useState } from 'react';
|
import { type HTMLAttributes, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useParams } from 'next/navigation';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
|
|
||||||
import { MenuIcon, SearchIcon } from 'lucide-react';
|
import { MenuIcon, SearchIcon } from 'lucide-react';
|
||||||
|
import { Link, useLocation, useParams } from 'react-router';
|
||||||
|
|
||||||
|
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
||||||
import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
|
import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
|
||||||
import { getRootHref } from '@documenso/lib/utils/params';
|
import { getRootHref } from '@documenso/lib/utils/params';
|
||||||
import type { User } from '@documenso/prisma/client';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
import { Logo } from '~/components/branding/logo';
|
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||||
|
|
||||||
import { CommandMenu } from '../common/command-menu';
|
import { AppCommandMenu } from './app-command-menu';
|
||||||
import { DesktopNav } from './desktop-nav';
|
import { AppNavDesktop } from './app-nav-desktop';
|
||||||
|
import { AppNavMobile } from './app-nav-mobile';
|
||||||
import { MenuSwitcher } from './menu-switcher';
|
import { MenuSwitcher } from './menu-switcher';
|
||||||
import { MobileNavigation } from './mobile-navigation';
|
|
||||||
|
|
||||||
export type HeaderProps = HTMLAttributes<HTMLDivElement> & {
|
export type HeaderProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
user: User;
|
user: SessionUser;
|
||||||
teams: TGetTeamsResponse;
|
teams: TGetTeamsResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
const [isCommandMenuOpen, setIsCommandMenuOpen] = useState(false);
|
const [isCommandMenuOpen, setIsCommandMenuOpen] = useState(false);
|
||||||
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
|
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
|
||||||
@ -42,8 +38,6 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
|||||||
return () => window.removeEventListener('scroll', onScroll);
|
return () => window.removeEventListener('scroll', onScroll);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const isPathTeamUrl = (teamUrl: string) => {
|
const isPathTeamUrl = (teamUrl: string) => {
|
||||||
if (!pathname || !pathname.startsWith(`/t/`)) {
|
if (!pathname || !pathname.startsWith(`/t/`)) {
|
||||||
return false;
|
return false;
|
||||||
@ -65,13 +59,13 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
|||||||
>
|
>
|
||||||
<div className="mx-auto flex w-full max-w-screen-xl items-center justify-between gap-x-4 px-4 md:justify-normal md:px-8">
|
<div className="mx-auto flex w-full max-w-screen-xl items-center justify-between gap-x-4 px-4 md:justify-normal md:px-8">
|
||||||
<Link
|
<Link
|
||||||
href={`${getRootHref(params, { returnEmptyRootString: true })}/documents`}
|
to={`${getRootHref(params, { returnEmptyRootString: true })}/documents`}
|
||||||
className="focus-visible:ring-ring ring-offset-background hidden rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 md:inline"
|
className="focus-visible:ring-ring ring-offset-background hidden rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 md:inline"
|
||||||
>
|
>
|
||||||
<Logo className="h-6 w-auto" />
|
<BrandingLogo className="h-6 w-auto" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<DesktopNav setIsCommandMenuOpen={setIsCommandMenuOpen} />
|
<AppNavDesktop setIsCommandMenuOpen={setIsCommandMenuOpen} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex gap-x-4 md:ml-8"
|
className="flex gap-x-4 md:ml-8"
|
||||||
@ -89,9 +83,9 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
|||||||
<MenuIcon className="text-muted-foreground h-6 w-6" />
|
<MenuIcon className="text-muted-foreground h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<CommandMenu open={isCommandMenuOpen} onOpenChange={setIsCommandMenuOpen} />
|
<AppCommandMenu open={isCommandMenuOpen} onOpenChange={setIsCommandMenuOpen} />
|
||||||
|
|
||||||
<MobileNavigation
|
<AppNavMobile
|
||||||
isMenuOpen={isHamburgerMenuOpen}
|
isMenuOpen={isHamburgerMenuOpen}
|
||||||
onMenuOpenChange={setIsHamburgerMenuOpen}
|
onMenuOpenChange={setIsHamburgerMenuOpen}
|
||||||
/>
|
/>
|
||||||
@ -1,12 +1,11 @@
|
|||||||
import type { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useParams, usePathname } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
|
import { Link, useLocation, useParams } from 'react-router';
|
||||||
|
|
||||||
import { getRootHref } from '@documenso/lib/utils/params';
|
import { getRootHref } from '@documenso/lib/utils/params';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@ -23,14 +22,18 @@ const navigationLinks = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export type DesktopNavProps = HTMLAttributes<HTMLDivElement> & {
|
export type AppNavDesktopProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
setIsCommandMenuOpen: (value: boolean) => void;
|
setIsCommandMenuOpen: (value: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: DesktopNavProps) => {
|
export const AppNavDesktop = ({
|
||||||
|
className,
|
||||||
|
setIsCommandMenuOpen,
|
||||||
|
...props
|
||||||
|
}: AppNavDesktopProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const pathname = usePathname();
|
const { pathname } = useLocation();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const [modifierKey, setModifierKey] = useState(() => 'Ctrl');
|
const [modifierKey, setModifierKey] = useState(() => 'Ctrl');
|
||||||
@ -56,7 +59,7 @@ export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: Deskto
|
|||||||
{navigationLinks.map(({ href, label }) => (
|
{navigationLinks.map(({ href, label }) => (
|
||||||
<Link
|
<Link
|
||||||
key={href}
|
key={href}
|
||||||
href={`${rootHref}${href}`}
|
to={`${rootHref}${href}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground dark:text-muted-foreground/60 focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2',
|
'text-muted-foreground dark:text-muted-foreground/60 focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2',
|
||||||
{
|
{
|
||||||
@ -1,24 +1,20 @@
|
|||||||
'use client';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useParams } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { signOut } from 'next-auth/react';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { Link, useParams } from 'react-router';
|
||||||
|
|
||||||
import LogoImage from '@documenso/assets/logo.png';
|
import LogoImage from '@documenso/assets/logo.png';
|
||||||
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { getRootHref } from '@documenso/lib/utils/params';
|
import { getRootHref } from '@documenso/lib/utils/params';
|
||||||
import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet';
|
import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet';
|
||||||
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
|
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
|
||||||
|
|
||||||
export type MobileNavigationProps = {
|
export type AppNavMobileProps = {
|
||||||
isMenuOpen: boolean;
|
isMenuOpen: boolean;
|
||||||
onMenuOpenChange?: (_value: boolean) => void;
|
onMenuOpenChange?: (_value: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigationProps) => {
|
export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@ -51,8 +47,8 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
|||||||
return (
|
return (
|
||||||
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
|
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
|
||||||
<SheetContent className="flex w-full max-w-[350px] flex-col">
|
<SheetContent className="flex w-full max-w-[350px] flex-col">
|
||||||
<Link href="/" onClick={handleMenuItemClick}>
|
<Link to="/" onClick={handleMenuItemClick}>
|
||||||
<Image
|
<img
|
||||||
src={LogoImage}
|
src={LogoImage}
|
||||||
alt="Documenso Logo"
|
alt="Documenso Logo"
|
||||||
className="dark:invert"
|
className="dark:invert"
|
||||||
@ -66,7 +62,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
|||||||
<Link
|
<Link
|
||||||
key={href}
|
key={href}
|
||||||
className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
|
className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
|
||||||
href={href}
|
to={href}
|
||||||
onClick={() => handleMenuItemClick()}
|
onClick={() => handleMenuItemClick()}
|
||||||
>
|
>
|
||||||
{_(text)}
|
{_(text)}
|
||||||
@ -75,11 +71,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
|
className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
|
||||||
onClick={async () =>
|
onClick={async () => authClient.signOut()}
|
||||||
signOut({
|
|
||||||
callbackUrl: '/',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Trans>Sign Out</Trans>
|
<Trans>Sign Out</Trans>
|
||||||
</button>
|
</button>
|
||||||
@ -1,17 +1,13 @@
|
|||||||
'use client';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import type { Recipient } from '@prisma/client';
|
||||||
|
import { DocumentStatus } from '@prisma/client';
|
||||||
|
|
||||||
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
||||||
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
@ -2,7 +2,7 @@ import type { SVGAttributes } from 'react';
|
|||||||
|
|
||||||
export type LogoProps = SVGAttributes<SVGSVGElement>;
|
export type LogoProps = SVGAttributes<SVGSVGElement>;
|
||||||
|
|
||||||
export const Logo = ({ ...props }: LogoProps) => {
|
export const BrandingLogo = ({ ...props }: LogoProps) => {
|
||||||
return (
|
return (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2248 320" {...props}>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2248 320" {...props}>
|
||||||
<path
|
<path
|
||||||
@ -1,16 +1,14 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -25,7 +23,7 @@ import { Input } from '@documenso/ui/primitives/input';
|
|||||||
import { PasswordInput } from '@documenso/ui/primitives/password-input';
|
import { PasswordInput } from '@documenso/ui/primitives/password-input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { signupErrorMessages } from '~/components/forms/v2/signup';
|
import { signupErrorMessages } from '~/components/forms/signup';
|
||||||
|
|
||||||
export type ClaimAccountProps = {
|
export type ClaimAccountProps = {
|
||||||
defaultName: string;
|
defaultName: string;
|
||||||
@ -60,9 +58,7 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const analytics = useAnalytics();
|
const analytics = useAnalytics();
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<TClaimAccountFormSchema>({
|
const form = useForm<TClaimAccountFormSchema>({
|
||||||
values: {
|
values: {
|
||||||
@ -75,9 +71,9 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
|
|||||||
|
|
||||||
const onFormSubmit = async ({ name, email, password }: TClaimAccountFormSchema) => {
|
const onFormSubmit = async ({ name, email, password }: TClaimAccountFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await signup({ name, email, password });
|
await authClient.emailPassword.signUp({ name, email, password });
|
||||||
|
|
||||||
router.push(`/unverified-account`);
|
await navigate(`/unverified-account`);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Registration Successful`),
|
title: _(msg`Registration Successful`),
|
||||||
@ -1,15 +1,14 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Recipient } from '@prisma/client';
|
||||||
|
import type { Field } from '@prisma/client';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import type { TTemplate } from '@documenso/lib/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 {
|
import {
|
||||||
DocumentFlowFormContainerActions,
|
DocumentFlowFormContainerActions,
|
||||||
DocumentFlowFormContainerContent,
|
DocumentFlowFormContainerContent,
|
||||||
@ -30,36 +29,37 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useStep } from '@documenso/ui/primitives/stepper';
|
import { useStep } from '@documenso/ui/primitives/stepper';
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from '~/app/(signing)/sign/[token]/document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from '~/components/general/document-signing/document-signing-auth-provider';
|
||||||
|
|
||||||
const ZConfigureDirectTemplateFormSchema = z.object({
|
const ZDirectTemplateConfigureFormSchema = z.object({
|
||||||
email: z.string().email('Email is invalid'),
|
email: z.string().email('Email is invalid'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TConfigureDirectTemplateFormSchema = z.infer<typeof ZConfigureDirectTemplateFormSchema>;
|
export type TDirectTemplateConfigureFormSchema = z.infer<typeof ZDirectTemplateConfigureFormSchema>;
|
||||||
|
|
||||||
export type ConfigureDirectTemplateFormProps = {
|
export type DirectTemplateConfigureFormProps = {
|
||||||
flowStep: DocumentFlowStep;
|
flowStep: DocumentFlowStep;
|
||||||
isDocumentPdfLoaded: boolean;
|
isDocumentPdfLoaded: boolean;
|
||||||
template: Omit<TTemplate, 'user'>;
|
template: Omit<TTemplate, 'user'>;
|
||||||
directTemplateRecipient: Recipient & { fields: Field[] };
|
directTemplateRecipient: Recipient & { fields: Field[] };
|
||||||
initialEmail?: string;
|
initialEmail?: string;
|
||||||
onSubmit: (_data: TConfigureDirectTemplateFormSchema) => void;
|
onSubmit: (_data: TDirectTemplateConfigureFormSchema) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ConfigureDirectTemplateFormPartial = ({
|
export const DirectTemplateConfigureForm = ({
|
||||||
flowStep,
|
flowStep,
|
||||||
isDocumentPdfLoaded,
|
isDocumentPdfLoaded,
|
||||||
template,
|
template,
|
||||||
directTemplateRecipient,
|
directTemplateRecipient,
|
||||||
initialEmail,
|
initialEmail,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: ConfigureDirectTemplateFormProps) => {
|
}: DirectTemplateConfigureFormProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { data: session } = useSession();
|
|
||||||
|
const { user } = useOptionalSession();
|
||||||
|
|
||||||
const { recipients } = template;
|
const { recipients } = template;
|
||||||
const { derivedRecipientAccessAuth } = useRequiredDocumentAuthContext();
|
const { derivedRecipientAccessAuth } = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
const recipientsWithBlankDirectRecipientEmail = recipients.map((recipient) => {
|
const recipientsWithBlankDirectRecipientEmail = recipients.map((recipient) => {
|
||||||
if (recipient.id === directTemplateRecipient.id) {
|
if (recipient.id === directTemplateRecipient.id) {
|
||||||
@ -72,9 +72,9 @@ export const ConfigureDirectTemplateFormPartial = ({
|
|||||||
return recipient;
|
return recipient;
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<TConfigureDirectTemplateFormSchema>({
|
const form = useForm<TDirectTemplateConfigureFormSchema>({
|
||||||
resolver: zodResolver(
|
resolver: zodResolver(
|
||||||
ZConfigureDirectTemplateFormSchema.superRefine((items, ctx) => {
|
ZDirectTemplateConfigureFormSchema.superRefine((items, ctx) => {
|
||||||
if (template.recipients.map((recipient) => recipient.email).includes(items.email)) {
|
if (template.recipients.map((recipient) => recipient.email).includes(items.email)) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
@ -125,7 +125,7 @@ export const ConfigureDirectTemplateFormPartial = ({
|
|||||||
disabled={
|
disabled={
|
||||||
field.disabled ||
|
field.disabled ||
|
||||||
derivedRecipientAccessAuth !== null ||
|
derivedRecipientAccessAuth !== null ||
|
||||||
session?.user.email !== undefined
|
user?.email !== undefined
|
||||||
}
|
}
|
||||||
placeholder="recipient@documenso.com"
|
placeholder="recipient@documenso.com"
|
||||||
/>
|
/>
|
||||||
@ -1,16 +1,13 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import type { Field } from '@prisma/client';
|
||||||
|
import { type Recipient } from '@prisma/client';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router';
|
||||||
|
|
||||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||||
import type { TTemplate } from '@documenso/lib/types/template';
|
import type { TTemplate } from '@documenso/lib/types/template';
|
||||||
import type { Field } from '@documenso/prisma/client';
|
|
||||||
import { type Recipient } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
|
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
|
||||||
@ -19,15 +16,19 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
|||||||
import { Stepper } from '@documenso/ui/primitives/stepper';
|
import { Stepper } from '@documenso/ui/primitives/stepper';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from '~/app/(signing)/sign/[token]/document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from '~/components/general/document-signing/document-signing-auth-provider';
|
||||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
import { useRequiredDocumentSigningContext } from '~/components/general/document-signing/document-signing-provider';
|
||||||
|
|
||||||
import type { TConfigureDirectTemplateFormSchema } from './configure-direct-template';
|
import {
|
||||||
import { ConfigureDirectTemplateFormPartial } from './configure-direct-template';
|
DirectTemplateConfigureForm,
|
||||||
import type { DirectTemplateLocalField } from './sign-direct-template';
|
type TDirectTemplateConfigureFormSchema,
|
||||||
import { SignDirectTemplateForm } from './sign-direct-template';
|
} from './direct-template-configure-form';
|
||||||
|
import {
|
||||||
|
type DirectTemplateLocalField,
|
||||||
|
DirectTemplateSigningForm,
|
||||||
|
} from './direct-template-signing-form';
|
||||||
|
|
||||||
export type TemplatesDirectPageViewProps = {
|
export type DirectTemplatePageViewProps = {
|
||||||
template: Omit<TTemplate, 'user'>;
|
template: Omit<TTemplate, 'user'>;
|
||||||
directTemplateToken: string;
|
directTemplateToken: string;
|
||||||
directTemplateRecipient: Recipient & { fields: Field[] };
|
directTemplateRecipient: Recipient & { fields: Field[] };
|
||||||
@ -40,15 +41,15 @@ export const DirectTemplatePageView = ({
|
|||||||
template,
|
template,
|
||||||
directTemplateRecipient,
|
directTemplateRecipient,
|
||||||
directTemplateToken,
|
directTemplateToken,
|
||||||
}: TemplatesDirectPageViewProps) => {
|
}: DirectTemplatePageViewProps) => {
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const searchParams = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { email, fullName, setEmail } = useRequiredSigningContext();
|
const { email, fullName, setEmail } = useRequiredDocumentSigningContext();
|
||||||
const { recipient, setRecipient } = useRequiredDocumentAuthContext();
|
const { recipient, setRecipient } = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
const [step, setStep] = useState<DirectTemplateStep>('configure');
|
const [step, setStep] = useState<DirectTemplateStep>('configure');
|
||||||
const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false);
|
const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false);
|
||||||
@ -76,7 +77,7 @@ export const DirectTemplatePageView = ({
|
|||||||
/**
|
/**
|
||||||
* Set the email into a temporary recipient so it can be used for reauth and signing email fields.
|
* Set the email into a temporary recipient so it can be used for reauth and signing email fields.
|
||||||
*/
|
*/
|
||||||
const onConfigureDirectTemplateSubmit = ({ email }: TConfigureDirectTemplateFormSchema) => {
|
const onConfigureDirectTemplateSubmit = ({ email }: TDirectTemplateConfigureFormSchema) => {
|
||||||
setEmail(email);
|
setEmail(email);
|
||||||
|
|
||||||
setRecipient({
|
setRecipient({
|
||||||
@ -112,7 +113,7 @@ export const DirectTemplatePageView = ({
|
|||||||
|
|
||||||
const redirectUrl = template.templateMeta?.redirectUrl;
|
const redirectUrl = template.templateMeta?.redirectUrl;
|
||||||
|
|
||||||
redirectUrl ? router.push(redirectUrl) : router.push(`/sign/${token}/complete`);
|
await (redirectUrl ? navigate(redirectUrl) : navigate(`/sign/${token}/complete`));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Something went wrong`),
|
title: _(msg`Something went wrong`),
|
||||||
@ -152,7 +153,7 @@ export const DirectTemplatePageView = ({
|
|||||||
currentStep={currentDocumentFlow.stepIndex}
|
currentStep={currentDocumentFlow.stepIndex}
|
||||||
setCurrentStep={(step) => setStep(DirectTemplateSteps[step - 1])}
|
setCurrentStep={(step) => setStep(DirectTemplateSteps[step - 1])}
|
||||||
>
|
>
|
||||||
<ConfigureDirectTemplateFormPartial
|
<DirectTemplateConfigureForm
|
||||||
flowStep={directTemplateFlow.configure}
|
flowStep={directTemplateFlow.configure}
|
||||||
template={template}
|
template={template}
|
||||||
directTemplateRecipient={directTemplateRecipient}
|
directTemplateRecipient={directTemplateRecipient}
|
||||||
@ -161,7 +162,7 @@ export const DirectTemplatePageView = ({
|
|||||||
initialEmail={email}
|
initialEmail={email}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SignDirectTemplateForm
|
<DirectTemplateSigningForm
|
||||||
flowStep={directTemplateFlow.sign}
|
flowStep={directTemplateFlow.sign}
|
||||||
directRecipient={recipient}
|
directRecipient={recipient}
|
||||||
directRecipientFields={directTemplateRecipient.fields}
|
directRecipientFields={directTemplateRecipient.fields}
|
||||||
@ -1,11 +1,10 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { signOut } from 'next-auth/react';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
|
||||||
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
@ -19,9 +18,7 @@ export const DirectTemplateAuthPageView = () => {
|
|||||||
try {
|
try {
|
||||||
setIsSigningOut(true);
|
setIsSigningOut(true);
|
||||||
|
|
||||||
await signOut({
|
await authClient.signOut();
|
||||||
callbackUrl: '/signin',
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Something went wrong`),
|
title: _(msg`Something went wrong`),
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Field, Recipient, Signature } from '@prisma/client';
|
||||||
|
import { FieldType } from '@prisma/client';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
@ -16,8 +18,6 @@ import {
|
|||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
import type { TTemplate } from '@documenso/lib/types/template';
|
import type { TTemplate } from '@documenso/lib/types/template';
|
||||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
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 {
|
import type {
|
||||||
TRemovedSignedFieldWithTokenMutationSchema,
|
TRemovedSignedFieldWithTokenMutationSchema,
|
||||||
TSignFieldWithTokenMutationSchema,
|
TSignFieldWithTokenMutationSchema,
|
||||||
@ -38,21 +38,20 @@ import { Label } from '@documenso/ui/primitives/label';
|
|||||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useStep } from '@documenso/ui/primitives/stepper';
|
import { useStep } from '@documenso/ui/primitives/stepper';
|
||||||
|
|
||||||
import { CheckboxField } from '~/app/(signing)/sign/[token]/checkbox-field';
|
import { DocumentSigningCheckboxField } from '~/components/general/document-signing/document-signing-checkbox-field';
|
||||||
import { DateField } from '~/app/(signing)/sign/[token]/date-field';
|
import { DocumentSigningCompleteDialog } from '~/components/general/document-signing/document-signing-complete-dialog';
|
||||||
import { DropdownField } from '~/app/(signing)/sign/[token]/dropdown-field';
|
import { DocumentSigningDateField } from '~/components/general/document-signing/document-signing-date-field';
|
||||||
import { EmailField } from '~/app/(signing)/sign/[token]/email-field';
|
import { DocumentSigningDropdownField } from '~/components/general/document-signing/document-signing-dropdown-field';
|
||||||
import { InitialsField } from '~/app/(signing)/sign/[token]/initials-field';
|
import { DocumentSigningEmailField } from '~/components/general/document-signing/document-signing-email-field';
|
||||||
import { NameField } from '~/app/(signing)/sign/[token]/name-field';
|
import { DocumentSigningInitialsField } from '~/components/general/document-signing/document-signing-initials-field';
|
||||||
import { NumberField } from '~/app/(signing)/sign/[token]/number-field';
|
import { DocumentSigningNameField } from '~/components/general/document-signing/document-signing-name-field';
|
||||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
import { DocumentSigningNumberField } from '~/components/general/document-signing/document-signing-number-field';
|
||||||
import { RadioField } from '~/app/(signing)/sign/[token]/radio-field';
|
import { useRequiredDocumentSigningContext } from '~/components/general/document-signing/document-signing-provider';
|
||||||
import { RecipientProvider } from '~/app/(signing)/sign/[token]/recipient-context';
|
import { DocumentSigningRadioField } from '~/components/general/document-signing/document-signing-radio-field';
|
||||||
import { SignDialog } from '~/app/(signing)/sign/[token]/sign-dialog';
|
import { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-field';
|
||||||
import { SignatureField } from '~/app/(signing)/sign/[token]/signature-field';
|
import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
|
||||||
import { TextField } from '~/app/(signing)/sign/[token]/text-field';
|
|
||||||
|
|
||||||
export type SignDirectTemplateFormProps = {
|
export type DirectTemplateSigningFormProps = {
|
||||||
flowStep: DocumentFlowStep;
|
flowStep: DocumentFlowStep;
|
||||||
directRecipient: Recipient;
|
directRecipient: Recipient;
|
||||||
directRecipientFields: Field[];
|
directRecipientFields: Field[];
|
||||||
@ -65,15 +64,15 @@ export type DirectTemplateLocalField = Field & {
|
|||||||
signature?: Signature;
|
signature?: Signature;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SignDirectTemplateForm = ({
|
export const DirectTemplateSigningForm = ({
|
||||||
flowStep,
|
flowStep,
|
||||||
directRecipient,
|
directRecipient,
|
||||||
directRecipientFields,
|
directRecipientFields,
|
||||||
template,
|
template,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: SignDirectTemplateFormProps) => {
|
}: DirectTemplateSigningFormProps) => {
|
||||||
const { fullName, signature, signatureValid, setFullName, setSignature } =
|
const { fullName, signature, signatureValid, setFullName, setSignature } =
|
||||||
useRequiredSigningContext();
|
useRequiredDocumentSigningContext();
|
||||||
|
|
||||||
const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(directRecipientFields);
|
const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(directRecipientFields);
|
||||||
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||||
@ -170,7 +169,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecipientProvider recipient={directRecipient}>
|
<>
|
||||||
<DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} />
|
<DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} />
|
||||||
|
|
||||||
<DocumentFlowFormContainerContent>
|
<DocumentFlowFormContainerContent>
|
||||||
@ -184,34 +183,37 @@ export const SignDirectTemplateForm = ({
|
|||||||
{localFields.map((field) =>
|
{localFields.map((field) =>
|
||||||
match(field.type)
|
match(field.type)
|
||||||
.with(FieldType.SIGNATURE, () => (
|
.with(FieldType.SIGNATURE, () => (
|
||||||
<SignatureField
|
<DocumentSigningSignatureField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
|
recipient={directRecipient}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
|
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.INITIALS, () => (
|
.with(FieldType.INITIALS, () => (
|
||||||
<InitialsField
|
<DocumentSigningInitialsField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
|
recipient={directRecipient}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.NAME, () => (
|
.with(FieldType.NAME, () => (
|
||||||
<NameField
|
<DocumentSigningNameField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
|
recipient={directRecipient}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.DATE, () => (
|
.with(FieldType.DATE, () => (
|
||||||
<DateField
|
<DocumentSigningDateField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
|
recipient={directRecipient}
|
||||||
dateFormat={template.templateMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
|
dateFormat={template.templateMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
|
||||||
timezone={template.templateMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE}
|
timezone={template.templateMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
@ -219,9 +221,10 @@ export const SignDirectTemplateForm = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.EMAIL, () => (
|
.with(FieldType.EMAIL, () => (
|
||||||
<EmailField
|
<DocumentSigningEmailField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
|
recipient={directRecipient}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@ -232,12 +235,13 @@ export const SignDirectTemplateForm = ({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<DocumentSigningTextField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={{
|
field={{
|
||||||
...field,
|
...field,
|
||||||
fieldMeta: parsedFieldMeta,
|
fieldMeta: parsedFieldMeta,
|
||||||
}}
|
}}
|
||||||
|
recipient={directRecipient}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@ -249,12 +253,13 @@ export const SignDirectTemplateForm = ({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NumberField
|
<DocumentSigningNumberField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={{
|
field={{
|
||||||
...field,
|
...field,
|
||||||
fieldMeta: parsedFieldMeta,
|
fieldMeta: parsedFieldMeta,
|
||||||
}}
|
}}
|
||||||
|
recipient={directRecipient}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@ -266,12 +271,13 @@ export const SignDirectTemplateForm = ({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownField
|
<DocumentSigningDropdownField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={{
|
field={{
|
||||||
...field,
|
...field,
|
||||||
fieldMeta: parsedFieldMeta,
|
fieldMeta: parsedFieldMeta,
|
||||||
}}
|
}}
|
||||||
|
recipient={directRecipient}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@ -283,12 +289,13 @@ export const SignDirectTemplateForm = ({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RadioField
|
<DocumentSigningRadioField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={{
|
field={{
|
||||||
...field,
|
...field,
|
||||||
fieldMeta: parsedFieldMeta,
|
fieldMeta: parsedFieldMeta,
|
||||||
}}
|
}}
|
||||||
|
recipient={directRecipient}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@ -300,12 +307,13 @@ export const SignDirectTemplateForm = ({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CheckboxField
|
<DocumentSigningCheckboxField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={{
|
field={{
|
||||||
...field,
|
...field,
|
||||||
fieldMeta: parsedFieldMeta,
|
fieldMeta: parsedFieldMeta,
|
||||||
}}
|
}}
|
||||||
|
recipient={directRecipient}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@ -343,7 +351,6 @@ export const SignDirectTemplateForm = ({
|
|||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setSignature(value);
|
setSignature(value);
|
||||||
}}
|
}}
|
||||||
allowTypedSignature={template.templateMeta?.typedSignatureEnabled}
|
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@ -366,7 +373,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
<Trans>Back</Trans>
|
<Trans>Back</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<SignDialog
|
<DocumentSigningCompleteDialog
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
onSignatureComplete={handleSubmit}
|
onSignatureComplete={handleSubmit}
|
||||||
documentTitle={template.title}
|
documentTitle={template.title}
|
||||||
@ -376,6 +383,6 @@ export const SignDirectTemplateForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DocumentFlowFormContainerFooter>
|
</DocumentFlowFormContainerFooter>
|
||||||
</RecipientProvider>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { RecipientRole } from '@prisma/client';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { DocumentAuth, type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import { DocumentAuth, type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { RecipientRole } from '@documenso/prisma/client';
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { DialogFooter } from '@documenso/ui/primitives/dialog';
|
import { DialogFooter } from '@documenso/ui/primitives/dialog';
|
||||||
@ -23,9 +23,9 @@ import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/
|
|||||||
|
|
||||||
import { EnableAuthenticatorAppDialog } from '~/components/forms/2fa/enable-authenticator-app-dialog';
|
import { EnableAuthenticatorAppDialog } from '~/components/forms/2fa/enable-authenticator-app-dialog';
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
|
|
||||||
export type DocumentActionAuth2FAProps = {
|
export type DocumentSigningAuth2FAProps = {
|
||||||
actionTarget?: 'FIELD' | 'DOCUMENT';
|
actionTarget?: 'FIELD' | 'DOCUMENT';
|
||||||
actionVerb?: string;
|
actionVerb?: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -42,15 +42,15 @@ const Z2FAAuthFormSchema = z.object({
|
|||||||
|
|
||||||
type T2FAAuthFormSchema = z.infer<typeof Z2FAAuthFormSchema>;
|
type T2FAAuthFormSchema = z.infer<typeof Z2FAAuthFormSchema>;
|
||||||
|
|
||||||
export const DocumentActionAuth2FA = ({
|
export const DocumentSigningAuth2FA = ({
|
||||||
actionTarget = 'FIELD',
|
actionTarget = 'FIELD',
|
||||||
actionVerb = 'sign',
|
actionVerb = 'sign',
|
||||||
onReauthFormSubmit,
|
onReauthFormSubmit,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: DocumentActionAuth2FAProps) => {
|
}: DocumentSigningAuth2FAProps) => {
|
||||||
const { recipient, user, isCurrentlyAuthenticating, setIsCurrentlyAuthenticating } =
|
const { recipient, user, isCurrentlyAuthenticating, setIsCurrentlyAuthenticating } =
|
||||||
useRequiredDocumentAuthContext();
|
useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
const form = useForm<T2FAAuthFormSchema>({
|
const form = useForm<T2FAAuthFormSchema>({
|
||||||
resolver: zodResolver(Z2FAAuthFormSchema),
|
resolver: zodResolver(Z2FAAuthFormSchema),
|
||||||
@ -109,17 +109,14 @@ export const DocumentActionAuth2FA = ({
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{user?.identityProvider === 'DOCUMENSO' && (
|
|
||||||
<p className="mt-2">
|
<p className="mt-2">
|
||||||
<Trans>
|
<Trans>
|
||||||
By enabling 2FA, you will be required to enter a code from your authenticator app
|
By enabling 2FA, you will be required to enter a code from your authenticator app
|
||||||
every time you sign in.
|
every time you sign in using email password.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
)}
|
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
||||||
<Trans>Close</Trans>
|
<Trans>Close</Trans>
|
||||||
@ -1,31 +1,32 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
|
import { RecipientRole } from '@prisma/client';
|
||||||
|
|
||||||
import { Trans } from '@lingui/macro';
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { signOut } from 'next-auth/react';
|
|
||||||
|
|
||||||
import { RecipientRole } from '@documenso/prisma/client';
|
|
||||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { DialogFooter } from '@documenso/ui/primitives/dialog';
|
import { DialogFooter } from '@documenso/ui/primitives/dialog';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
|
|
||||||
export type DocumentActionAuthAccountProps = {
|
export type DocumentSigningAuthAccountProps = {
|
||||||
actionTarget?: 'FIELD' | 'DOCUMENT';
|
actionTarget?: 'FIELD' | 'DOCUMENT';
|
||||||
actionVerb?: string;
|
actionVerb?: string;
|
||||||
onOpenChange: (value: boolean) => void;
|
onOpenChange: (value: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentActionAuthAccount = ({
|
export const DocumentSigningAuthAccount = ({
|
||||||
actionTarget = 'FIELD',
|
actionTarget = 'FIELD',
|
||||||
actionVerb = 'sign',
|
actionVerb = 'sign',
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: DocumentActionAuthAccountProps) => {
|
}: DocumentSigningAuthAccountProps) => {
|
||||||
const { recipient } = useRequiredDocumentAuthContext();
|
const { recipient } = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
const router = useRouter();
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
const [isSigningOut, setIsSigningOut] = useState(false);
|
const [isSigningOut, setIsSigningOut] = useState(false);
|
||||||
|
|
||||||
@ -33,15 +34,18 @@ export const DocumentActionAuthAccount = ({
|
|||||||
try {
|
try {
|
||||||
setIsSigningOut(true);
|
setIsSigningOut(true);
|
||||||
|
|
||||||
await signOut({
|
await authClient.signOut({
|
||||||
redirect: false,
|
redirectPath: `/signin#email=${email}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push(`/signin#email=${email}`);
|
|
||||||
} catch {
|
} catch {
|
||||||
setIsSigningOut(false);
|
setIsSigningOut(false);
|
||||||
|
|
||||||
// Todo: Alert.
|
toast({
|
||||||
|
title: t`Something went wrong`,
|
||||||
|
description: t`We were unable to log you out at this time.`,
|
||||||
|
duration: 10000,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { FieldType } from '@prisma/client';
|
||||||
import { P, match } from 'ts-pattern';
|
import { P, match } from 'ts-pattern';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -6,7 +7,6 @@ import {
|
|||||||
type TRecipientActionAuth,
|
type TRecipientActionAuth,
|
||||||
type TRecipientActionAuthTypes,
|
type TRecipientActionAuthTypes,
|
||||||
} from '@documenso/lib/types/document-auth';
|
} from '@documenso/lib/types/document-auth';
|
||||||
import type { FieldType } from '@documenso/prisma/client';
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -15,12 +15,12 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
|
||||||
import { DocumentActionAuth2FA } from './document-action-auth-2fa';
|
import { DocumentSigningAuth2FA } from './document-signing-auth-2fa';
|
||||||
import { DocumentActionAuthAccount } from './document-action-auth-account';
|
import { DocumentSigningAuthAccount } from './document-signing-auth-account';
|
||||||
import { DocumentActionAuthPasskey } from './document-action-auth-passkey';
|
import { DocumentSigningAuthPasskey } from './document-signing-auth-passkey';
|
||||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
|
|
||||||
export type DocumentActionAuthDialogProps = {
|
export type DocumentSigningAuthDialogProps = {
|
||||||
title?: string;
|
title?: string;
|
||||||
documentAuthType: TRecipientActionAuthTypes;
|
documentAuthType: TRecipientActionAuthTypes;
|
||||||
description?: string;
|
description?: string;
|
||||||
@ -34,15 +34,15 @@ export type DocumentActionAuthDialogProps = {
|
|||||||
onReauthFormSubmit: (values?: TRecipientActionAuth) => Promise<void> | void;
|
onReauthFormSubmit: (values?: TRecipientActionAuth) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentActionAuthDialog = ({
|
export const DocumentSigningAuthDialog = ({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
documentAuthType,
|
documentAuthType,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onReauthFormSubmit,
|
onReauthFormSubmit,
|
||||||
}: DocumentActionAuthDialogProps) => {
|
}: DocumentSigningAuthDialogProps) => {
|
||||||
const { recipient, user, isCurrentlyAuthenticating } = useRequiredDocumentAuthContext();
|
const { recipient, user, isCurrentlyAuthenticating } = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
const handleOnOpenChange = (value: boolean) => {
|
const handleOnOpenChange = (value: boolean) => {
|
||||||
if (isCurrentlyAuthenticating) {
|
if (isCurrentlyAuthenticating) {
|
||||||
@ -67,17 +67,17 @@ export const DocumentActionAuthDialog = ({
|
|||||||
.with(
|
.with(
|
||||||
{ documentAuthType: DocumentAuth.ACCOUNT },
|
{ documentAuthType: DocumentAuth.ACCOUNT },
|
||||||
{ user: P.when((user) => !user || user.email !== recipient.email) }, // Assume all current auth methods requires them to be logged in.
|
{ user: P.when((user) => !user || user.email !== recipient.email) }, // Assume all current auth methods requires them to be logged in.
|
||||||
() => <DocumentActionAuthAccount onOpenChange={onOpenChange} />,
|
() => <DocumentSigningAuthAccount onOpenChange={onOpenChange} />,
|
||||||
)
|
)
|
||||||
.with({ documentAuthType: DocumentAuth.PASSKEY }, () => (
|
.with({ documentAuthType: DocumentAuth.PASSKEY }, () => (
|
||||||
<DocumentActionAuthPasskey
|
<DocumentSigningAuthPasskey
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
onReauthFormSubmit={onReauthFormSubmit}
|
onReauthFormSubmit={onReauthFormSubmit}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with({ documentAuthType: DocumentAuth.TWO_FACTOR_AUTH }, () => (
|
.with({ documentAuthType: DocumentAuth.TWO_FACTOR_AUTH }, () => (
|
||||||
<DocumentActionAuth2FA
|
<DocumentSigningAuth2FA
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
onReauthFormSubmit={onReauthFormSubmit}
|
onReauthFormSubmit={onReauthFormSubmit}
|
||||||
@ -1,38 +1,34 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { signOut } from 'next-auth/react';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
|
||||||
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type SigningAuthPageViewProps = {
|
export type DocumentSigningAuthPageViewProps = {
|
||||||
email: string;
|
email: string;
|
||||||
emailHasAccount?: boolean;
|
emailHasAccount?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SigningAuthPageView = ({ email, emailHasAccount }: SigningAuthPageViewProps) => {
|
export const DocumentSigningAuthPageView = ({
|
||||||
|
email,
|
||||||
|
emailHasAccount,
|
||||||
|
}: DocumentSigningAuthPageViewProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [isSigningOut, setIsSigningOut] = useState(false);
|
const [isSigningOut, setIsSigningOut] = useState(false);
|
||||||
|
|
||||||
const handleChangeAccount = async (email: string) => {
|
const handleChangeAccount = async (email: string) => {
|
||||||
try {
|
try {
|
||||||
setIsSigningOut(true);
|
setIsSigningOut(true);
|
||||||
|
|
||||||
await signOut({
|
await authClient.signOut({
|
||||||
redirect: false,
|
redirectPath: emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push(emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`);
|
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Something went wrong`),
|
title: _(msg`Something went wrong`),
|
||||||
@ -1,8 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { RecipientRole } from '@prisma/client';
|
||||||
import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser';
|
import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
@ -10,7 +12,6 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { DocumentAuth, type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import { DocumentAuth, type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { RecipientRole } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -31,11 +32,11 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@documenso/ui/primitives/select';
|
} from '@documenso/ui/primitives/select';
|
||||||
|
|
||||||
import { CreatePasskeyDialog } from '~/app/(dashboard)/settings/security/passkeys/create-passkey-dialog';
|
import { PasskeyCreateDialog } from '~/components/dialogs/passkey-create-dialog';
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
|
|
||||||
export type DocumentActionAuthPasskeyProps = {
|
export type DocumentSigningAuthPasskeyProps = {
|
||||||
actionTarget?: 'FIELD' | 'DOCUMENT';
|
actionTarget?: 'FIELD' | 'DOCUMENT';
|
||||||
actionVerb?: string;
|
actionVerb?: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -49,13 +50,13 @@ const ZPasskeyAuthFormSchema = z.object({
|
|||||||
|
|
||||||
type TPasskeyAuthFormSchema = z.infer<typeof ZPasskeyAuthFormSchema>;
|
type TPasskeyAuthFormSchema = z.infer<typeof ZPasskeyAuthFormSchema>;
|
||||||
|
|
||||||
export const DocumentActionAuthPasskey = ({
|
export const DocumentSigningAuthPasskey = ({
|
||||||
actionTarget = 'FIELD',
|
actionTarget = 'FIELD',
|
||||||
actionVerb = 'sign',
|
actionVerb = 'sign',
|
||||||
onReauthFormSubmit,
|
onReauthFormSubmit,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: DocumentActionAuthPasskeyProps) => {
|
}: DocumentSigningAuthPasskeyProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -66,7 +67,7 @@ export const DocumentActionAuthPasskey = ({
|
|||||||
isCurrentlyAuthenticating,
|
isCurrentlyAuthenticating,
|
||||||
setIsCurrentlyAuthenticating,
|
setIsCurrentlyAuthenticating,
|
||||||
refetchPasskeys,
|
refetchPasskeys,
|
||||||
} = useRequiredDocumentAuthContext();
|
} = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
const form = useForm<TPasskeyAuthFormSchema>({
|
const form = useForm<TPasskeyAuthFormSchema>({
|
||||||
resolver: zodResolver(ZPasskeyAuthFormSchema),
|
resolver: zodResolver(ZPasskeyAuthFormSchema),
|
||||||
@ -189,7 +190,7 @@ export const DocumentActionAuthPasskey = ({
|
|||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<CreatePasskeyDialog
|
<PasskeyCreateDialog
|
||||||
onSuccess={async () => refetchPasskeys()}
|
onSuccess={async () => refetchPasskeys()}
|
||||||
trigger={
|
trigger={
|
||||||
<Button>
|
<Button>
|
||||||
@ -1,9 +1,9 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { type Document, FieldType, type Passkey, type Recipient } from '@prisma/client';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
|
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
||||||
import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth';
|
import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth';
|
||||||
import type {
|
import type {
|
||||||
TDocumentAuthOptions,
|
TDocumentAuthOptions,
|
||||||
@ -13,17 +13,10 @@ import type {
|
|||||||
} from '@documenso/lib/types/document-auth';
|
} from '@documenso/lib/types/document-auth';
|
||||||
import { DocumentAuth } from '@documenso/lib/types/document-auth';
|
import { DocumentAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||||
import {
|
|
||||||
type Document,
|
|
||||||
FieldType,
|
|
||||||
type Passkey,
|
|
||||||
type Recipient,
|
|
||||||
type User,
|
|
||||||
} from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
|
||||||
import type { DocumentActionAuthDialogProps } from './document-action-auth-dialog';
|
import type { DocumentSigningAuthDialogProps } from './document-signing-auth-dialog';
|
||||||
import { DocumentActionAuthDialog } from './document-action-auth-dialog';
|
import { DocumentSigningAuthDialog } from './document-signing-auth-dialog';
|
||||||
|
|
||||||
type PasskeyData = {
|
type PasskeyData = {
|
||||||
passkeys: Omit<Passkey, 'credentialId' | 'credentialPublicKey'>[];
|
passkeys: Omit<Passkey, 'credentialId' | 'credentialPublicKey'>[];
|
||||||
@ -32,7 +25,7 @@ type PasskeyData = {
|
|||||||
isError: boolean;
|
isError: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DocumentAuthContextValue = {
|
export type DocumentSigningAuthContextValue = {
|
||||||
executeActionAuthProcedure: (_value: ExecuteActionAuthProcedureOptions) => Promise<void>;
|
executeActionAuthProcedure: (_value: ExecuteActionAuthProcedureOptions) => Promise<void>;
|
||||||
documentAuthOptions: Document['authOptions'];
|
documentAuthOptions: Document['authOptions'];
|
||||||
documentAuthOption: TDocumentAuthOptions;
|
documentAuthOption: TDocumentAuthOptions;
|
||||||
@ -48,39 +41,39 @@ export type DocumentAuthContextValue = {
|
|||||||
passkeyData: PasskeyData;
|
passkeyData: PasskeyData;
|
||||||
preferredPasskeyId: string | null;
|
preferredPasskeyId: string | null;
|
||||||
setPreferredPasskeyId: (_value: string | null) => void;
|
setPreferredPasskeyId: (_value: string | null) => void;
|
||||||
user?: User | null;
|
user?: SessionUser | null;
|
||||||
refetchPasskeys: () => Promise<void>;
|
refetchPasskeys: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DocumentAuthContext = createContext<DocumentAuthContextValue | null>(null);
|
const DocumentSigningAuthContext = createContext<DocumentSigningAuthContextValue | null>(null);
|
||||||
|
|
||||||
export const useDocumentAuthContext = () => {
|
export const useDocumentSigningAuthContext = () => {
|
||||||
return useContext(DocumentAuthContext);
|
return useContext(DocumentSigningAuthContext);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useRequiredDocumentAuthContext = () => {
|
export const useRequiredDocumentSigningAuthContext = () => {
|
||||||
const context = useDocumentAuthContext();
|
const context = useDocumentSigningAuthContext();
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('Document auth context is required');
|
throw new Error('Document signing auth context is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface DocumentAuthProviderProps {
|
export interface DocumentSigningAuthProviderProps {
|
||||||
documentAuthOptions: Document['authOptions'];
|
documentAuthOptions: Document['authOptions'];
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
user?: User | null;
|
user?: SessionUser | null;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DocumentAuthProvider = ({
|
export const DocumentSigningAuthProvider = ({
|
||||||
documentAuthOptions: initialDocumentAuthOptions,
|
documentAuthOptions: initialDocumentAuthOptions,
|
||||||
recipient: initialRecipient,
|
recipient: initialRecipient,
|
||||||
user,
|
user,
|
||||||
children,
|
children,
|
||||||
}: DocumentAuthProviderProps) => {
|
}: DocumentSigningAuthProviderProps) => {
|
||||||
const [documentAuthOptions, setDocumentAuthOptions] = useState(initialDocumentAuthOptions);
|
const [documentAuthOptions, setDocumentAuthOptions] = useState(initialDocumentAuthOptions);
|
||||||
const [recipient, setRecipient] = useState(initialRecipient);
|
const [recipient, setRecipient] = useState(initialRecipient);
|
||||||
|
|
||||||
@ -186,7 +179,7 @@ export const DocumentAuthProvider = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentAuthContext.Provider
|
<DocumentSigningAuthContext.Provider
|
||||||
value={{
|
value={{
|
||||||
user,
|
user,
|
||||||
documentAuthOptions,
|
documentAuthOptions,
|
||||||
@ -210,7 +203,7 @@ export const DocumentAuthProvider = ({
|
|||||||
{children}
|
{children}
|
||||||
|
|
||||||
{documentAuthDialogPayload && derivedRecipientActionAuth && (
|
{documentAuthDialogPayload && derivedRecipientActionAuth && (
|
||||||
<DocumentActionAuthDialog
|
<DocumentSigningAuthDialog
|
||||||
open={true}
|
open={true}
|
||||||
onOpenChange={() => setDocumentAuthDialogPayload(null)}
|
onOpenChange={() => setDocumentAuthDialogPayload(null)}
|
||||||
onReauthFormSubmit={documentAuthDialogPayload.onReauthFormSubmit}
|
onReauthFormSubmit={documentAuthDialogPayload.onReauthFormSubmit}
|
||||||
@ -218,13 +211,13 @@ export const DocumentAuthProvider = ({
|
|||||||
documentAuthType={derivedRecipientActionAuth}
|
documentAuthType={derivedRecipientActionAuth}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</DocumentAuthContext.Provider>
|
</DocumentSigningAuthContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type ExecuteActionAuthProcedureOptions = Omit<
|
type ExecuteActionAuthProcedureOptions = Omit<
|
||||||
DocumentActionAuthDialogProps,
|
DocumentSigningAuthDialogProps,
|
||||||
'open' | 'onOpenChange' | 'documentAuthType' | 'recipientRole'
|
'open' | 'onOpenChange' | 'documentAuthType' | 'recipientRole'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
DocumentAuthProvider.displayName = 'DocumentAuthProvider';
|
DocumentSigningAuthProvider.displayName = 'DocumentSigningAuthProvider';
|
||||||
@ -1,19 +1,17 @@
|
|||||||
'use client';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useState, useTransition } from 'react';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Plural, Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Plural, Trans } from '@lingui/react/macro';
|
||||||
|
import type { Field, Recipient } from '@prisma/client';
|
||||||
|
import { FieldType } from '@prisma/client';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
import { P, match } from 'ts-pattern';
|
import { P, match } from 'ts-pattern';
|
||||||
|
|
||||||
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
|
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
|
||||||
import { DocumentAuth } from '@documenso/lib/types/document-auth';
|
import { DocumentAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
|
||||||
import { FieldType } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -27,10 +25,10 @@ import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/type
|
|||||||
import { Form } from '@documenso/ui/primitives/form/form';
|
import { Form } from '@documenso/ui/primitives/form/form';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { SigningDisclosure } from '~/components/general/signing-disclosure';
|
import { DocumentSigningDisclosure } from '~/components/general/document-signing/document-signing-disclosure';
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
import { useRequiredSigningContext } from './provider';
|
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||||
|
|
||||||
const AUTO_SIGNABLE_FIELD_TYPES: string[] = [
|
const AUTO_SIGNABLE_FIELD_TYPES: string[] = [
|
||||||
FieldType.NAME,
|
FieldType.NAME,
|
||||||
@ -55,22 +53,20 @@ const NON_AUTO_SIGNABLE_ACTION_AUTH_TYPES: string[] = [
|
|||||||
// while for larger documents with many fields it will be beneficial to sign away the boilerplate fields.
|
// while for larger documents with many fields it will be beneficial to sign away the boilerplate fields.
|
||||||
const AUTO_SIGN_THRESHOLD = 5;
|
const AUTO_SIGN_THRESHOLD = 5;
|
||||||
|
|
||||||
export type AutoSignProps = {
|
export type DocumentSigningAutoSignProps = {
|
||||||
recipient: Pick<Recipient, 'id' | 'token'>;
|
recipient: Pick<Recipient, 'id' | 'token'>;
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AutoSign = ({ recipient, fields }: AutoSignProps) => {
|
export const DocumentSigningAutoSign = ({ recipient, fields }: DocumentSigningAutoSignProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const router = useRouter();
|
const { email, fullName } = useRequiredDocumentSigningContext();
|
||||||
|
const { derivedRecipientActionAuth } = useRequiredDocumentSigningAuthContext();
|
||||||
const { email, fullName } = useRequiredSigningContext();
|
|
||||||
const { derivedRecipientActionAuth } = useRequiredDocumentAuthContext();
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
|
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
|
|
||||||
@ -158,11 +154,7 @@ export const AutoSign = ({ recipient, fields }: AutoSignProps) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
startTransition(() => {
|
await revalidate();
|
||||||
router.refresh();
|
|
||||||
|
|
||||||
setOpen(false);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
unsafe_useEffectOnce(() => {
|
unsafe_useEffectOnce(() => {
|
||||||
@ -205,7 +197,7 @@ export const AutoSign = ({ recipient, fields }: AutoSignProps) => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SigningDisclosure className="mt-4" />
|
<DocumentSigningDisclosure className="mt-4" />
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
@ -223,7 +215,7 @@ export const AutoSign = ({ recipient, fields }: AutoSignProps) => {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="min-w-[6rem]"
|
className="min-w-[6rem]"
|
||||||
loading={form.formState.isSubmitting || isPending}
|
loading={form.formState.isSubmitting}
|
||||||
disabled={!autoSignableFields.length}
|
disabled={!autoSignableFields.length}
|
||||||
>
|
>
|
||||||
<Trans>Sign</Trans>
|
<Trans>Sign</Trans>
|
||||||
@ -1,12 +1,10 @@
|
|||||||
'use client';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useEffect, useMemo, useState, useTransition } from 'react';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import type { Recipient } from '@prisma/client';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
@ -25,24 +23,27 @@ import { checkboxValidationSigns } from '@documenso/ui/primitives/document-flow/
|
|||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
import { useRecipientContext } from './recipient-context';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
import { SigningFieldContainer } from './signing-field-container';
|
|
||||||
|
|
||||||
export type CheckboxFieldProps = {
|
export type DocumentSigningCheckboxFieldProps = {
|
||||||
field: FieldWithSignatureAndFieldMeta;
|
field: FieldWithSignatureAndFieldMeta;
|
||||||
|
recipient: Recipient;
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFieldProps) => {
|
export const DocumentSigningCheckboxField = ({
|
||||||
|
field,
|
||||||
|
recipient,
|
||||||
|
onSignField,
|
||||||
|
onUnsignField,
|
||||||
|
}: DocumentSigningCheckboxFieldProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const router = useRouter();
|
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
|
|
||||||
|
|
||||||
const parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
const parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
||||||
|
|
||||||
@ -84,7 +85,7 @@ export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFie
|
|||||||
isPending: isRemoveSignedFieldWithTokenLoading,
|
isPending: isRemoveSignedFieldWithTokenLoading,
|
||||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
|
||||||
const shouldAutoSignField =
|
const shouldAutoSignField =
|
||||||
(!field.inserted && checkedValues.length > 0 && isLengthConditionMet) ||
|
(!field.inserted && checkedValues.length > 0 && isLengthConditionMet) ||
|
||||||
(!field.inserted && isReadOnly && isLengthConditionMet);
|
(!field.inserted && isReadOnly && isLengthConditionMet);
|
||||||
@ -105,7 +106,7 @@ export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFie
|
|||||||
await signFieldWithToken(payload);
|
await signFieldWithToken(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -117,9 +118,7 @@ export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFie
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: isAssistantMode
|
description: _(msg`An error occurred while signing the document.`),
|
||||||
? _(msg`An error occurred while signing as assistant.`)
|
|
||||||
: _(msg`An error occurred while signing the document.`),
|
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -142,13 +141,13 @@ export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFie
|
|||||||
setCheckedValues([]);
|
setCheckedValues([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while removing the field.`),
|
description: _(msg`An error occurred while removing the signature.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -180,27 +179,30 @@ export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFie
|
|||||||
...checkedValues,
|
...checkedValues,
|
||||||
item.value.length > 0 ? item.value : `empty-value-${item.id}`,
|
item.value.length > 0 ? item.value : `empty-value-${item.id}`,
|
||||||
];
|
];
|
||||||
} else {
|
|
||||||
updatedValues = checkedValues.filter(
|
|
||||||
(v) => v !== item.value && v !== `empty-value-${item.id}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setCheckedValues(updatedValues);
|
|
||||||
|
|
||||||
await removeSignedFieldWithToken({
|
await removeSignedFieldWithToken({
|
||||||
token: recipient.token,
|
token: recipient.token,
|
||||||
fieldId: field.id,
|
fieldId: field.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (updatedValues.length > 0) {
|
if (isLengthConditionMet) {
|
||||||
await signFieldWithToken({
|
await signFieldWithToken({
|
||||||
token: recipient.token,
|
token: recipient.token,
|
||||||
fieldId: field.id,
|
fieldId: field.id,
|
||||||
value: toCheckboxValue(updatedValues),
|
value: toCheckboxValue(checkedValues),
|
||||||
isBase64: true,
|
isBase64: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
updatedValues = checkedValues.filter(
|
||||||
|
(v) => v !== item.value && v !== `empty-value-${item.id}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await removeSignedFieldWithToken({
|
||||||
|
token: recipient.token,
|
||||||
|
fieldId: field.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
@ -210,7 +212,8 @@ export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFie
|
|||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
startTransition(() => router.refresh());
|
setCheckedValues(updatedValues);
|
||||||
|
await revalidate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -229,7 +232,12 @@ export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFie
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Checkbox">
|
<DocumentSigningFieldContainer
|
||||||
|
field={field}
|
||||||
|
onSign={onSign}
|
||||||
|
onRemove={onRemove}
|
||||||
|
type="Checkbox"
|
||||||
|
>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="bg-background absolute inset-0 z-20 flex items-center justify-center rounded-md">
|
<div className="bg-background absolute inset-0 z-20 flex items-center justify-center rounded-md">
|
||||||
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
||||||
@ -287,6 +295,6 @@ export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFie
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</SigningFieldContainer>
|
</DocumentSigningFieldContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -0,0 +1,150 @@
|
|||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Field } from '@prisma/client';
|
||||||
|
import { RecipientRole } from '@prisma/client';
|
||||||
|
|
||||||
|
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
|
||||||
|
import { DocumentSigningDisclosure } from '~/components/general/document-signing/document-signing-disclosure';
|
||||||
|
|
||||||
|
export type DocumentSigningCompleteDialogProps = {
|
||||||
|
isSubmitting: boolean;
|
||||||
|
documentTitle: string;
|
||||||
|
fields: Field[];
|
||||||
|
fieldsValidated: () => void | Promise<void>;
|
||||||
|
onSignatureComplete: () => void | Promise<void>;
|
||||||
|
role: RecipientRole;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DocumentSigningCompleteDialog = ({
|
||||||
|
isSubmitting,
|
||||||
|
documentTitle,
|
||||||
|
fields,
|
||||||
|
fieldsValidated,
|
||||||
|
onSignatureComplete,
|
||||||
|
role,
|
||||||
|
disabled = false,
|
||||||
|
}: DocumentSigningCompleteDialogProps) => {
|
||||||
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
|
|
||||||
|
const isComplete = useMemo(() => !fieldsContainUnsignedRequiredField(fields), [fields]);
|
||||||
|
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
if (isSubmitting || !isComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowDialog(open);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={showDialog} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
type="button"
|
||||||
|
size="lg"
|
||||||
|
onClick={fieldsValidated}
|
||||||
|
loading={isSubmitting}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{isComplete ? <Trans>Complete</Trans> : <Trans>Next field</Trans>}
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<DialogTitle>
|
||||||
|
<div className="text-foreground text-xl font-semibold">
|
||||||
|
{role === RecipientRole.VIEWER && <Trans>Complete Viewing</Trans>}
|
||||||
|
{role === RecipientRole.SIGNER && <Trans>Complete Signing</Trans>}
|
||||||
|
{role === RecipientRole.APPROVER && <Trans>Complete Approval</Trans>}
|
||||||
|
</div>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<div className="text-muted-foreground max-w-[50ch]">
|
||||||
|
{role === RecipientRole.VIEWER && (
|
||||||
|
<span>
|
||||||
|
<Trans>
|
||||||
|
<span className="inline-flex flex-wrap">
|
||||||
|
You are about to complete viewing "
|
||||||
|
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
||||||
|
{documentTitle}
|
||||||
|
</span>
|
||||||
|
".
|
||||||
|
</span>
|
||||||
|
<br /> Are you sure?
|
||||||
|
</Trans>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{role === RecipientRole.SIGNER && (
|
||||||
|
<span>
|
||||||
|
<Trans>
|
||||||
|
<span className="inline-flex flex-wrap">
|
||||||
|
You are about to complete signing "
|
||||||
|
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
||||||
|
{documentTitle}
|
||||||
|
</span>
|
||||||
|
".
|
||||||
|
</span>
|
||||||
|
<br /> Are you sure?
|
||||||
|
</Trans>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{role === RecipientRole.APPROVER && (
|
||||||
|
<span>
|
||||||
|
<Trans>
|
||||||
|
<span className="inline-flex flex-wrap">
|
||||||
|
You are about to complete approving{' '}
|
||||||
|
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
||||||
|
"{documentTitle}"
|
||||||
|
</span>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
<br /> Are you sure?
|
||||||
|
</Trans>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DocumentSigningDisclosure className="mt-4" />
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setShowDialog(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="flex-1"
|
||||||
|
disabled={!isComplete}
|
||||||
|
loading={isSubmitting}
|
||||||
|
onClick={onSignatureComplete}
|
||||||
|
>
|
||||||
|
{role === RecipientRole.VIEWER && <Trans>Mark as Viewed</Trans>}
|
||||||
|
{role === RecipientRole.SIGNER && <Trans>Sign</Trans>}
|
||||||
|
{role === RecipientRole.APPROVER && <Trans>Approve</Trans>}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,12 +1,9 @@
|
|||||||
'use client';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { useTransition } from 'react';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Recipient } from '@prisma/client';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_DOCUMENT_DATE_FORMAT,
|
DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||||
@ -16,41 +13,36 @@ import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'
|
|||||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { ZDateFieldMeta } from '@documenso/lib/types/field-meta';
|
|
||||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type {
|
import type {
|
||||||
TRemovedSignedFieldWithTokenMutationSchema,
|
TRemovedSignedFieldWithTokenMutationSchema,
|
||||||
TSignFieldWithTokenMutationSchema,
|
TSignFieldWithTokenMutationSchema,
|
||||||
} from '@documenso/trpc/server/field-router/schema';
|
} from '@documenso/trpc/server/field-router/schema';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { useRecipientContext } from './recipient-context';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
import { SigningFieldContainer } from './signing-field-container';
|
|
||||||
|
|
||||||
export type DateFieldProps = {
|
export type DocumentSigningDateFieldProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
|
recipient: Recipient;
|
||||||
dateFormat?: string | null;
|
dateFormat?: string | null;
|
||||||
timezone?: string | null;
|
timezone?: string | null;
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DateField = ({
|
export const DocumentSigningDateField = ({
|
||||||
field,
|
field,
|
||||||
|
recipient,
|
||||||
dateFormat = DEFAULT_DOCUMENT_DATE_FORMAT,
|
dateFormat = DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||||
timezone = DEFAULT_DOCUMENT_TIME_ZONE,
|
timezone = DEFAULT_DOCUMENT_TIME_ZONE,
|
||||||
onSignField,
|
onSignField,
|
||||||
onUnsignField,
|
onUnsignField,
|
||||||
}: DateFieldProps) => {
|
}: DocumentSigningDateFieldProps) => {
|
||||||
const router = useRouter();
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
|
|
||||||
|
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
|
|
||||||
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
||||||
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
@ -60,15 +52,14 @@ export const DateField = ({
|
|||||||
isPending: isRemoveSignedFieldWithTokenLoading,
|
isPending: isRemoveSignedFieldWithTokenLoading,
|
||||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
const safeFieldMeta = ZDateFieldMeta.safeParse(field.fieldMeta);
|
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
|
||||||
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
|
|
||||||
|
|
||||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
|
||||||
|
|
||||||
const localDateString = convertToLocalSystemFormat(field.customText, dateFormat, timezone);
|
const localDateString = convertToLocalSystemFormat(field.customText, dateFormat, timezone);
|
||||||
|
|
||||||
const isDifferentTime = field.inserted && localDateString !== field.customText;
|
const isDifferentTime = field.inserted && localDateString !== field.customText;
|
||||||
|
|
||||||
const tooltipText = _(
|
const tooltipText = _(
|
||||||
msg`"${field.customText}" will appear on the document as it has a timezone of "${timezone}".`,
|
msg`"${field.customText}" will appear on the document as it has a timezone of "${timezone || ''}".`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
||||||
@ -87,7 +78,7 @@ export const DateField = ({
|
|||||||
|
|
||||||
await signFieldWithToken(payload);
|
await signFieldWithToken(payload);
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -99,9 +90,7 @@ export const DateField = ({
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: isAssistantMode
|
description: _(msg`An error occurred while signing the document.`),
|
||||||
? _(msg`An error occurred while signing as assistant.`)
|
|
||||||
: _(msg`An error occurred while signing the document.`),
|
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -121,20 +110,20 @@ export const DateField = ({
|
|||||||
|
|
||||||
await removeSignedFieldWithToken(payload);
|
await removeSignedFieldWithToken(payload);
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while removing the field.`),
|
description: _(msg`An error occurred while removing the signature.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigningFieldContainer
|
<DocumentSigningFieldContainer
|
||||||
field={field}
|
field={field}
|
||||||
onSign={onSign}
|
onSign={onSign}
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
@ -154,22 +143,10 @@ export const DateField = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{field.inserted && (
|
{field.inserted && (
|
||||||
<div className="flex h-full w-full items-center">
|
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.425rem,25cqw,0.825rem)] duration-200">
|
||||||
<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}
|
{localDateString}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</SigningFieldContainer>
|
</DocumentSigningFieldContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1,14 +1,16 @@
|
|||||||
import type { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { Link } from 'react-router';
|
||||||
import { Trans } from '@lingui/macro';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
export type SigningDisclosureProps = HTMLAttributes<HTMLParagraphElement>;
|
export type DocumentSigningDisclosureProps = HTMLAttributes<HTMLParagraphElement>;
|
||||||
|
|
||||||
export const SigningDisclosure = ({ className, ...props }: SigningDisclosureProps) => {
|
export const DocumentSigningDisclosure = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: DocumentSigningDisclosureProps) => {
|
||||||
return (
|
return (
|
||||||
<p className={cn('text-muted-foreground text-xs', className)} {...props}>
|
<p className={cn('text-muted-foreground text-xs', className)} {...props}>
|
||||||
<Trans>
|
<Trans>
|
||||||
@ -22,7 +24,7 @@ export const SigningDisclosure = ({ className, ...props }: SigningDisclosureProp
|
|||||||
Read the full{' '}
|
Read the full{' '}
|
||||||
<Link
|
<Link
|
||||||
className="text-documenso-700 underline"
|
className="text-documenso-700 underline"
|
||||||
href="/articles/signature-disclosure"
|
to="/articles/signature-disclosure"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
signature disclosure
|
signature disclosure
|
||||||
@ -1,12 +1,10 @@
|
|||||||
'use client';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useEffect, useState, useTransition } from 'react';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import type { Recipient } from '@prisma/client';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
@ -28,25 +26,27 @@ import {
|
|||||||
} from '@documenso/ui/primitives/select';
|
} from '@documenso/ui/primitives/select';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
import { useRecipientContext } from './recipient-context';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
import { SigningFieldContainer } from './signing-field-container';
|
|
||||||
|
|
||||||
export type DropdownFieldProps = {
|
export type DocumentSigningDropdownFieldProps = {
|
||||||
field: FieldWithSignatureAndFieldMeta;
|
field: FieldWithSignatureAndFieldMeta;
|
||||||
|
recipient: Recipient;
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DropdownField = ({ field, onSignField, onUnsignField }: DropdownFieldProps) => {
|
export const DocumentSigningDropdownField = ({
|
||||||
|
field,
|
||||||
|
recipient,
|
||||||
|
onSignField,
|
||||||
|
onUnsignField,
|
||||||
|
}: DocumentSigningDropdownFieldProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const router = useRouter();
|
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
|
|
||||||
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
|
|
||||||
|
|
||||||
const parsedFieldMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
|
const parsedFieldMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
|
||||||
const isReadOnly = parsedFieldMeta?.readOnly;
|
const isReadOnly = parsedFieldMeta?.readOnly;
|
||||||
@ -61,7 +61,7 @@ export const DropdownField = ({ field, onSignField, onUnsignField }: DropdownFie
|
|||||||
isPending: isRemoveSignedFieldWithTokenLoading,
|
isPending: isRemoveSignedFieldWithTokenLoading,
|
||||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
|
||||||
const shouldAutoSignField =
|
const shouldAutoSignField =
|
||||||
(!field.inserted && localChoice) || (!field.inserted && isReadOnly && defaultValue);
|
(!field.inserted && localChoice) || (!field.inserted && isReadOnly && defaultValue);
|
||||||
|
|
||||||
@ -86,7 +86,8 @@ export const DropdownField = ({ field, onSignField, onUnsignField }: DropdownFie
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLocalChoice('');
|
setLocalChoice('');
|
||||||
startTransition(() => router.refresh());
|
|
||||||
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -98,9 +99,7 @@ export const DropdownField = ({ field, onSignField, onUnsignField }: DropdownFie
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: isAssistantMode
|
description: _(msg`An error occurred while signing the document.`),
|
||||||
? _(msg`An error occurred while signing as assistant.`)
|
|
||||||
: _(msg`An error occurred while signing the document.`),
|
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -125,13 +124,14 @@ export const DropdownField = ({ field, onSignField, onUnsignField }: DropdownFie
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLocalChoice('');
|
setLocalChoice('');
|
||||||
startTransition(() => router.refresh());
|
|
||||||
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while removing the field.`),
|
description: _(msg`An error occurred while removing the signature.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -161,7 +161,7 @@ export const DropdownField = ({ field, onSignField, onUnsignField }: DropdownFie
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pointer-events-none">
|
<div className="pointer-events-none">
|
||||||
<SigningFieldContainer
|
<DocumentSigningFieldContainer
|
||||||
field={field}
|
field={field}
|
||||||
onPreSign={onPreSign}
|
onPreSign={onPreSign}
|
||||||
onSign={onSign}
|
onSign={onSign}
|
||||||
@ -207,7 +207,7 @@ export const DropdownField = ({ field, onSignField, onUnsignField }: DropdownFie
|
|||||||
{field.customText}
|
{field.customText}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</SigningFieldContainer>
|
</DocumentSigningFieldContainer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1,46 +1,42 @@
|
|||||||
'use client';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { useTransition } from 'react';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Recipient } from '@prisma/client';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { ZEmailFieldMeta } from '@documenso/lib/types/field-meta';
|
|
||||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type {
|
import type {
|
||||||
TRemovedSignedFieldWithTokenMutationSchema,
|
TRemovedSignedFieldWithTokenMutationSchema,
|
||||||
TSignFieldWithTokenMutationSchema,
|
TSignFieldWithTokenMutationSchema,
|
||||||
} from '@documenso/trpc/server/field-router/schema';
|
} from '@documenso/trpc/server/field-router/schema';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { useRequiredSigningContext } from './provider';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
import { useRecipientContext } from './recipient-context';
|
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||||
import { SigningFieldContainer } from './signing-field-container';
|
|
||||||
|
|
||||||
export type EmailFieldProps = {
|
export type DocumentSigningEmailFieldProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
|
recipient: Recipient;
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EmailField = ({ field, onSignField, onUnsignField }: EmailFieldProps) => {
|
export const DocumentSigningEmailField = ({
|
||||||
const router = useRouter();
|
field,
|
||||||
|
recipient,
|
||||||
|
onSignField,
|
||||||
|
onUnsignField,
|
||||||
|
}: DocumentSigningEmailFieldProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const { email: providedEmail } = useRequiredSigningContext();
|
const { email: providedEmail } = useRequiredDocumentSigningContext();
|
||||||
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
|
|
||||||
|
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
|
|
||||||
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
||||||
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
@ -50,10 +46,7 @@ export const EmailField = ({ field, onSignField, onUnsignField }: EmailFieldProp
|
|||||||
isPending: isRemoveSignedFieldWithTokenLoading,
|
isPending: isRemoveSignedFieldWithTokenLoading,
|
||||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
const safeFieldMeta = ZEmailFieldMeta.safeParse(field.fieldMeta);
|
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
|
||||||
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
|
|
||||||
|
|
||||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
|
||||||
|
|
||||||
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
||||||
try {
|
try {
|
||||||
@ -74,7 +67,7 @@ export const EmailField = ({ field, onSignField, onUnsignField }: EmailFieldProp
|
|||||||
|
|
||||||
await signFieldWithToken(payload);
|
await signFieldWithToken(payload);
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -86,9 +79,7 @@ export const EmailField = ({ field, onSignField, onUnsignField }: EmailFieldProp
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: isAssistantMode
|
description: _(msg`An error occurred while signing the document.`),
|
||||||
? _(msg`An error occurred while signing as assistant.`)
|
|
||||||
: _(msg`An error occurred while signing the document.`),
|
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -108,20 +99,20 @@ export const EmailField = ({ field, onSignField, onUnsignField }: EmailFieldProp
|
|||||||
|
|
||||||
await removeSignedFieldWithToken(payload);
|
await removeSignedFieldWithToken(payload);
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while removing the field.`),
|
description: _(msg`An error occurred while removing the signature.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Email">
|
<DocumentSigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Email">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
||||||
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
||||||
@ -135,22 +126,10 @@ export const EmailField = ({ field, onSignField, onUnsignField }: EmailFieldProp
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{field.inserted && (
|
{field.inserted && (
|
||||||
<div className="flex h-full w-full items-center">
|
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.425rem,25cqw,0.825rem)] duration-200">
|
||||||
<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}
|
{field.customText}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</SigningFieldContainer>
|
</DocumentSigningFieldContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1,21 +1,19 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { FieldType } from '@prisma/client';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
import { type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import { type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
import { FieldType } from '@documenso/prisma/client';
|
|
||||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
import { FieldRootContainer } from '@documenso/ui/components/field/field';
|
import { FieldRootContainer } from '@documenso/ui/components/field/field';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
|
|
||||||
export type SignatureFieldProps = {
|
export type DocumentSigningFieldContainerProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -46,7 +44,6 @@ export type SignatureFieldProps = {
|
|||||||
| 'Email'
|
| 'Email'
|
||||||
| 'Name'
|
| 'Name'
|
||||||
| 'Signature'
|
| 'Signature'
|
||||||
| 'Text'
|
|
||||||
| 'Radio'
|
| 'Radio'
|
||||||
| 'Dropdown'
|
| 'Dropdown'
|
||||||
| 'Number'
|
| 'Number'
|
||||||
@ -54,7 +51,7 @@ export type SignatureFieldProps = {
|
|||||||
tooltipText?: string | null;
|
tooltipText?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SigningFieldContainer = ({
|
export const DocumentSigningFieldContainer = ({
|
||||||
field,
|
field,
|
||||||
loading,
|
loading,
|
||||||
onPreSign,
|
onPreSign,
|
||||||
@ -63,8 +60,9 @@ export const SigningFieldContainer = ({
|
|||||||
children,
|
children,
|
||||||
type,
|
type,
|
||||||
tooltipText,
|
tooltipText,
|
||||||
}: SignatureFieldProps) => {
|
}: DocumentSigningFieldContainerProps) => {
|
||||||
const { executeActionAuthProcedure, isAuthRedirectRequired } = useRequiredDocumentAuthContext();
|
const { executeActionAuthProcedure, isAuthRedirectRequired } =
|
||||||
|
useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
const parsedFieldMeta = field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined;
|
const parsedFieldMeta = field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined;
|
||||||
const readOnlyField = parsedFieldMeta?.readOnly || false;
|
const readOnlyField = parsedFieldMeta?.readOnly || false;
|
||||||
@ -0,0 +1,268 @@
|
|||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { type Field, FieldType, type Recipient, RecipientRole } from '@prisma/client';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
|
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||||
|
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
||||||
|
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
|
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
|
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||||
|
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||||
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
|
|
||||||
|
import { DocumentSigningCompleteDialog } from './document-signing-complete-dialog';
|
||||||
|
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||||
|
|
||||||
|
export type DocumentSigningFormProps = {
|
||||||
|
document: DocumentAndSender;
|
||||||
|
recipient: Recipient;
|
||||||
|
fields: Field[];
|
||||||
|
redirectUrl?: string | null;
|
||||||
|
isRecipientsTurn: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DocumentSigningForm = ({
|
||||||
|
document,
|
||||||
|
recipient,
|
||||||
|
fields,
|
||||||
|
redirectUrl,
|
||||||
|
isRecipientsTurn,
|
||||||
|
}: DocumentSigningFormProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const analytics = useAnalytics();
|
||||||
|
|
||||||
|
const { user } = useOptionalSession();
|
||||||
|
|
||||||
|
const { fullName, signature, setFullName, setSignature, signatureValid, setSignatureValid } =
|
||||||
|
useRequiredDocumentSigningContext();
|
||||||
|
|
||||||
|
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||||
|
|
||||||
|
const { mutateAsync: completeDocumentWithToken } =
|
||||||
|
trpc.recipient.completeDocumentWithToken.useMutation();
|
||||||
|
|
||||||
|
const { handleSubmit, formState } = useForm();
|
||||||
|
|
||||||
|
// Keep the loading state going if successful since the redirect may take some time.
|
||||||
|
const isSubmitting = formState.isSubmitting || formState.isSubmitSuccessful;
|
||||||
|
|
||||||
|
const fieldsRequiringValidation = useMemo(
|
||||||
|
() => fields.filter(isFieldUnsignedAndRequired),
|
||||||
|
[fields],
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
|
||||||
|
|
||||||
|
const uninsertedFields = useMemo(() => {
|
||||||
|
return sortFieldsByPosition(fieldsRequiringValidation.filter((field) => !field.inserted));
|
||||||
|
}, [fields]);
|
||||||
|
|
||||||
|
const fieldsValidated = () => {
|
||||||
|
setValidateUninsertedFields(true);
|
||||||
|
validateFieldsInserted(fieldsRequiringValidation);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFormSubmit = async () => {
|
||||||
|
setValidateUninsertedFields(true);
|
||||||
|
|
||||||
|
const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation);
|
||||||
|
|
||||||
|
if (hasSignatureField && !signatureValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFieldsValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await completeDocument();
|
||||||
|
|
||||||
|
// Reauth is currently not required for completing the document.
|
||||||
|
// await executeActionAuthProcedure({
|
||||||
|
// onReauthFormSubmit: completeDocument,
|
||||||
|
// actionTarget: 'DOCUMENT',
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const completeDocument = async (authOptions?: TRecipientActionAuth) => {
|
||||||
|
await completeDocumentWithToken({
|
||||||
|
token: recipient.token,
|
||||||
|
documentId: document.id,
|
||||||
|
authOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
analytics.capture('App: Recipient has completed signing', {
|
||||||
|
signerId: recipient.id,
|
||||||
|
documentId: document.id,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (redirectUrl) {
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
} else {
|
||||||
|
await navigate(`/sign/${recipient.token}/complete`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
className={cn(
|
||||||
|
'dark:bg-background border-border bg-widget sticky flex h-full flex-col rounded-xl border px-4 py-6',
|
||||||
|
{
|
||||||
|
'top-20 max-h-[min(68rem,calc(100vh-6rem))]': user,
|
||||||
|
'top-4 max-h-[min(68rem,calc(100vh-2rem))]': !user,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
|
>
|
||||||
|
{validateUninsertedFields && uninsertedFields[0] && (
|
||||||
|
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
|
||||||
|
<Trans>Click to insert field</Trans>
|
||||||
|
</FieldToolTip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<fieldset
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className={cn(
|
||||||
|
'custom-scrollbar -mx-2 flex flex-1 flex-col overflow-y-auto overflow-x-hidden px-2',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={cn('flex flex-1 flex-col')}>
|
||||||
|
<h3 className="text-foreground text-2xl font-semibold">
|
||||||
|
{recipient.role === RecipientRole.VIEWER && <Trans>View Document</Trans>}
|
||||||
|
{recipient.role === RecipientRole.SIGNER && <Trans>Sign Document</Trans>}
|
||||||
|
{recipient.role === RecipientRole.APPROVER && <Trans>Approve Document</Trans>}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{recipient.role === RecipientRole.VIEWER ? (
|
||||||
|
<>
|
||||||
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
|
<Trans>Please mark as viewed to complete</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr className="border-border mb-8 mt-4" />
|
||||||
|
|
||||||
|
<div className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2">
|
||||||
|
<div className="flex flex-1 flex-col gap-y-4" />
|
||||||
|
<div className="flex flex-col gap-4 md:flex-row">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
||||||
|
variant="secondary"
|
||||||
|
size="lg"
|
||||||
|
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||||
|
onClick={async () => navigate(-1)}
|
||||||
|
>
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<DocumentSigningCompleteDialog
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
onSignatureComplete={handleSubmit(onFormSubmit)}
|
||||||
|
documentTitle={document.title}
|
||||||
|
fields={fields}
|
||||||
|
fieldsValidated={fieldsValidated}
|
||||||
|
role={recipient.role}
|
||||||
|
disabled={!isRecipientsTurn}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
|
<Trans>Please review the document before signing.</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr className="border-border mb-8 mt-4" />
|
||||||
|
|
||||||
|
<div className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2">
|
||||||
|
<div className="flex flex-1 flex-col gap-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="full-name">
|
||||||
|
<Trans>Full Name</Trans>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="full-name"
|
||||||
|
className="bg-background mt-2"
|
||||||
|
value={fullName}
|
||||||
|
onChange={(e) => setFullName(e.target.value.trimStart())}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="Signature">
|
||||||
|
<Trans>Signature</Trans>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Card className="mt-2" gradient degrees={-120}>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<SignaturePad
|
||||||
|
className="h-44 w-full"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
defaultValue={signature ?? undefined}
|
||||||
|
onValidityChange={(isValid) => {
|
||||||
|
setSignatureValid(isValid);
|
||||||
|
}}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (signatureValid) {
|
||||||
|
setSignature(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
allowTypedSignature={document.documentMeta?.typedSignatureEnabled}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{hasSignatureField && !signatureValid && (
|
||||||
|
<div className="text-destructive mt-2 text-sm">
|
||||||
|
<Trans>
|
||||||
|
Signature is too small. Please provide a more complete signature.
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 md:flex-row">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
||||||
|
variant="secondary"
|
||||||
|
size="lg"
|
||||||
|
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||||
|
onClick={async () => navigate(-1)}
|
||||||
|
>
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<DocumentSigningCompleteDialog
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
onSignatureComplete={handleSubmit(onFormSubmit)}
|
||||||
|
documentTitle={document.title}
|
||||||
|
fields={fields}
|
||||||
|
fieldsValidated={fieldsValidated}
|
||||||
|
role={recipient.role}
|
||||||
|
disabled={!isRecipientsTurn}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user