mirror of
https://github.com/documenso/documenso.git
synced 2025-11-23 05:01:54 +10:00
Compare commits
56 Commits
chore/dece
...
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 | |||
| 9183f668d3 | |||
| 54ea96391a | |||
| 42d24fd1a1 | |||
| dc36a8182c | |||
| 0ef85b47b1 | |||
| 058d9dd0ba | |||
| 74bb230247 | |||
| 7c1e0f34e8 | |||
| 7e31323faa | |||
| a28cdf437b | |||
| 80dfbeb16f | |||
| 9de3a32ceb | |||
| 0d3864548c | |||
| 9e03747e43 | |||
| 5750f2b477 | |||
| 901be70f97 | |||
| 7d0a9c6439 | |||
| 48b55758e3 | |||
| dcaccb65f2 | |||
| 723e1b4ea2 |
@ -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: {
|
||||||
|
|||||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -10,7 +10,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze
|
name: Analyze
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
|
|||||||
2
.github/workflows/e2e-tests.yml
vendored
2
.github/workflows/e2e-tests.yml
vendored
@ -8,7 +8,7 @@ jobs:
|
|||||||
e2e_tests:
|
e2e_tests:
|
||||||
name: 'E2E Tests'
|
name: 'E2E Tests'
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
runs-on: ubuntu-22.04
|
runs-on: warp-ubuntu-2204-x64-16x
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
"@documenso/trpc": "*",
|
"@documenso/trpc": "*",
|
||||||
"@documenso/ui": "*",
|
"@documenso/ui": "*",
|
||||||
"next": "14.2.23",
|
"next": "14.2.6",
|
||||||
"next-plausible": "^3.12.0",
|
"next-plausible": "^3.12.0",
|
||||||
"nextra": "^2.13.4",
|
"nextra": "^2.13.4",
|
||||||
"nextra-theme-docs": "^2.13.4",
|
"nextra-theme-docs": "^2.13.4",
|
||||||
@ -27,6 +27,6 @@
|
|||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5,5 +5,6 @@
|
|||||||
"svelte": "Svelte Integration",
|
"svelte": "Svelte Integration",
|
||||||
"solid": "Solid Integration",
|
"solid": "Solid Integration",
|
||||||
"preact": "Preact Integration",
|
"preact": "Preact Integration",
|
||||||
|
"angular": "Angular Integration",
|
||||||
"css-variables": "CSS Variables"
|
"css-variables": "CSS Variables"
|
||||||
}
|
}
|
||||||
|
|||||||
90
apps/documentation/pages/developers/embedding/angular.mdx
Normal file
90
apps/documentation/pages/developers/embedding/angular.mdx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
---
|
||||||
|
title: Angular Integration
|
||||||
|
description: Learn how to use our embedding SDK within your Angular application.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Angular Integration
|
||||||
|
|
||||||
|
Our Angular SDK provides a simple way to embed a signing experience within your Angular application. It supports both direct link templates and signing tokens.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To install the SDK, run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @documenso/embed-angular
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To embed a signing experience, you'll need to provide the token for the document you want to embed. This can be done in a few different ways, depending on your use case.
|
||||||
|
|
||||||
|
### Direct Link Template
|
||||||
|
|
||||||
|
If you have a direct link template, you can simply provide the token for the template to the `EmbedDirectTemplate` component.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { EmbedDirectTemplate } from '@documenso/embed-angular';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-embedding',
|
||||||
|
template: `
|
||||||
|
<embed-direct-template [token]="token" />
|
||||||
|
`,
|
||||||
|
standalone: true,
|
||||||
|
imports: [EmbedDirectTemplate],
|
||||||
|
})
|
||||||
|
export class EmbeddingComponent {
|
||||||
|
token = 'YOUR_TOKEN_HERE'; // Replace with the actual token
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Props
|
||||||
|
|
||||||
|
| Prop | Type | Description |
|
||||||
|
| ------------------- | ------------------- | ------------------------------------------------------------------------------------------ |
|
||||||
|
| token | string | The token for the document you want to embed |
|
||||||
|
| host | string (optional) | The host to be used for the signing experience, relevant for self-hosters |
|
||||||
|
| name | string (optional) | The name the signer that will be used by default for signing |
|
||||||
|
| lockName | boolean (optional) | Whether or not the name field should be locked disallowing modifications |
|
||||||
|
| email | string (optional) | The email the signer that will be used by default for signing |
|
||||||
|
| lockEmail | boolean (optional) | Whether or not the email field should be locked disallowing modifications |
|
||||||
|
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
|
||||||
|
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
|
||||||
|
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |
|
||||||
|
| onFieldSigned | function (optional) | A callback function that will be called when a field is signed |
|
||||||
|
| onFieldUnsigned | function (optional) | A callback function that will be called when a field is unsigned |
|
||||||
|
|
||||||
|
### Signing Token
|
||||||
|
|
||||||
|
If you have a signing token, you can provide it to the `EmbedSignDocument` component.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { EmbedSignDocument } from '@documenso/embed-angular';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-embedding',
|
||||||
|
template: `
|
||||||
|
<embed-sign-document [token]="token" />
|
||||||
|
`,
|
||||||
|
standalone: true,
|
||||||
|
imports: [EmbedSignDocument],
|
||||||
|
})
|
||||||
|
export class EmbeddingComponent {
|
||||||
|
token = 'YOUR_TOKEN_HERE'; // Replace with the actual token
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Props
|
||||||
|
|
||||||
|
| Prop | Type | Description |
|
||||||
|
| ------------------- | ------------------- | ------------------------------------------------------------------------------------------ |
|
||||||
|
| token | string | The token for the document you want to embed |
|
||||||
|
| host | string (optional) | The host to be used for the signing experience, relevant for self-hosters |
|
||||||
|
| name | string (optional) | The name the signer that will be used by default for signing |
|
||||||
|
| lockName | boolean (optional) | Whether or not the name field should be locked disallowing modifications |
|
||||||
|
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
|
||||||
|
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
|
||||||
|
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |
|
||||||
@ -5,7 +5,7 @@ description: Learn how to use embedding to bring signing to your own website or
|
|||||||
|
|
||||||
# Embedding
|
# Embedding
|
||||||
|
|
||||||
Our embedding feature lets you integrate our document signing experience into your own application or website. Whether you're building with React, Preact, Vue, Svelte, Solid, or using generalized web components, this guide will help you get started with embedding Documenso.
|
Our embedding feature lets you integrate our document signing experience into your own application or website. Whether you're building with React, Preact, Vue, Svelte, Solid, Angular, or using generalized web components, this guide will help you get started with embedding Documenso.
|
||||||
|
|
||||||
## Availability
|
## Availability
|
||||||
|
|
||||||
@ -73,13 +73,14 @@ These customization options are available for both Direct Templates and Signing
|
|||||||
|
|
||||||
We support embedding across a range of popular JavaScript frameworks, including:
|
We support embedding across a range of popular JavaScript frameworks, including:
|
||||||
|
|
||||||
| Framework | Package |
|
| Framework | Package |
|
||||||
| --------- | -------------------------------------------------------------------------------- |
|
| --------- | ---------------------------------------------------------------------------------- |
|
||||||
| React | [@documenso/embed-react](https://www.npmjs.com/package/@documenso/embed-react) |
|
| React | [@documenso/embed-react](https://www.npmjs.com/package/@documenso/embed-react) |
|
||||||
| Preact | [@documenso/embed-preact](https://www.npmjs.com/package/@documenso/embed-preact) |
|
| Preact | [@documenso/embed-preact](https://www.npmjs.com/package/@documenso/embed-preact) |
|
||||||
| Vue | [@documenso/embed-vue](https://www.npmjs.com/package/@documenso/embed-vue) |
|
| Vue | [@documenso/embed-vue](https://www.npmjs.com/package/@documenso/embed-vue) |
|
||||||
| Svelte | [@documenso/embed-svelte](https://www.npmjs.com/package/@documenso/embed-svelte) |
|
| Svelte | [@documenso/embed-svelte](https://www.npmjs.com/package/@documenso/embed-svelte) |
|
||||||
| Solid | [@documenso/embed-solid](https://www.npmjs.com/package/@documenso/embed-solid) |
|
| Solid | [@documenso/embed-solid](https://www.npmjs.com/package/@documenso/embed-solid) |
|
||||||
|
| Angular | [@documenso/embed-angular](https://www.npmjs.com/package/@documenso/embed-angular) |
|
||||||
|
|
||||||
Additionally, we provide **web components** for more generalized use. However, please note that web components are still in their early stages and haven't been extensively tested.
|
Additionally, we provide **web components** for more generalized use. However, please note that web components are still in their early stages and haven't been extensively tested.
|
||||||
|
|
||||||
@ -127,7 +128,7 @@ This will show a dialog which will ask you to configure which recipient should b
|
|||||||
|
|
||||||
## Embedding with Signing Tokens
|
## Embedding with Signing Tokens
|
||||||
|
|
||||||
To embed the signing process for an ordinary document, you’ll need a **document signing token** for the recipient. This token provides the necessary access to load the document and facilitate the signing process securely.
|
To embed the signing process for an ordinary document, you'll need a **document signing token** for the recipient. This token provides the necessary access to load the document and facilitate the signing process securely.
|
||||||
|
|
||||||
#### Instructions
|
#### Instructions
|
||||||
|
|
||||||
@ -164,6 +165,7 @@ Once you've obtained the appropriate tokens, you can integrate the signing exper
|
|||||||
- [Vue](/developers/embedding/vue)
|
- [Vue](/developers/embedding/vue)
|
||||||
- [Svelte](/developers/embedding/svelte)
|
- [Svelte](/developers/embedding/svelte)
|
||||||
- [Solid](/developers/embedding/solid)
|
- [Solid](/developers/embedding/solid)
|
||||||
|
- [Angular](/developers/embedding/angular)
|
||||||
|
|
||||||
If you're using **web components**, the integration process is slightly different. Keep in mind that web components are currently less tested but can still provide flexibility for general use cases.
|
If you're using **web components**, the integration process is slightly different. Keep in mind that web components are currently less tested but can still provide flexibility for general use cases.
|
||||||
|
|
||||||
@ -174,4 +176,5 @@ If you're using **web components**, the integration process is slightly differen
|
|||||||
- [Svelte Integration](/developers/embedding/svelte)
|
- [Svelte Integration](/developers/embedding/svelte)
|
||||||
- [Solid Integration](/developers/embedding/solid)
|
- [Solid Integration](/developers/embedding/solid)
|
||||||
- [Preact Integration](/developers/embedding/preact)
|
- [Preact Integration](/developers/embedding/preact)
|
||||||
|
- [Angular Integration](/developers/embedding/angular)
|
||||||
- [CSS Variables](/developers/embedding/css-variables)
|
- [CSS Variables](/developers/embedding/css-variables)
|
||||||
|
|||||||
@ -13,11 +13,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
"next": "14.2.23"
|
"next": "14.2.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "18.3.5",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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();
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProp
|
|||||||
|
|
||||||
const [enteredEmail, setEnteredEmail] = useState<string>('');
|
const [enteredEmail, setEnteredEmail] = useState<string>('');
|
||||||
|
|
||||||
const { mutateAsync: deleteAccount, isLoading: isDeletingAccount } =
|
const { mutateAsync: deleteAccount, isPending: isDeletingAccount } =
|
||||||
trpc.profile.deleteAccount.useMutation();
|
trpc.profile.deleteAccount.useMutation();
|
||||||
|
|
||||||
const onDeleteAccount = async () => {
|
const onDeleteAccount = async () => {
|
||||||
@ -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,14 +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 { TRPCClientError } from '@documenso/trpc/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';
|
||||||
@ -24,19 +21,19 @@ 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('');
|
||||||
|
|
||||||
const { mutateAsync: deleteDocument, isLoading: isDeletingDocument } =
|
const { mutateAsync: deleteDocument, isPending: isDeletingDocument } =
|
||||||
trpc.admin.deleteDocument.useMutation();
|
trpc.admin.deleteDocument.useMutation();
|
||||||
|
|
||||||
const handleDeleteDocument = async () => {
|
const handleDeleteDocument = async () => {
|
||||||
@ -53,23 +50,14 @@ export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialo
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push('/admin/documents');
|
await navigate('/admin/documents');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
toast({
|
||||||
toast({
|
title: _(msg`An unknown error occurred`),
|
||||||
title: _(msg`An error occurred`),
|
variant: 'destructive',
|
||||||
description: err.message,
|
description:
|
||||||
variant: 'destructive',
|
'We encountered an unknown error while attempting to delete your document. Please try again later.',
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
toast({
|
|
||||||
title: _(msg`An unknown error occurred`),
|
|
||||||
variant: 'destructive',
|
|
||||||
description:
|
|
||||||
err.message ??
|
|
||||||
'We encountered an unknown error while attempting to delete your document. Please try again later.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -77,7 +65,7 @@ export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialo
|
|||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<Alert
|
<Alert
|
||||||
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row "
|
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
|
||||||
variant="neutral"
|
variant="neutral"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@ -1,14 +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 type { User } from '@documenso/prisma/client';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { TRPCClientError } from '@documenso/trpc/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';
|
||||||
@ -24,20 +23,18 @@ 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, isLoading: isDeletingUser } =
|
const { mutateAsync: deleteUser, isPending: isDeletingUser } =
|
||||||
trpc.admin.deleteUser.useMutation();
|
trpc.admin.deleteUser.useMutation();
|
||||||
|
|
||||||
const onDeleteAccount = async () => {
|
const onDeleteAccount = async () => {
|
||||||
@ -46,31 +43,27 @@ 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) {
|
||||||
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
const error = AppError.parseError(err);
|
||||||
toast({
|
|
||||||
title: _(msg`An error occurred`),
|
const errorMessage = match(error.code)
|
||||||
description: err.message,
|
.with(AppErrorCode.NOT_FOUND, () => msg`User not found.`)
|
||||||
variant: 'destructive',
|
.with(AppErrorCode.UNAUTHORIZED, () => msg`You are not authorized to delete this user.`)
|
||||||
});
|
.otherwise(() => msg`An error occurred while deleting the user.`);
|
||||||
} else {
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`Error`),
|
||||||
variant: 'destructive',
|
description: _(errorMessage),
|
||||||
description:
|
variant: 'destructive',
|
||||||
err.message ??
|
duration: 7500,
|
||||||
_(
|
});
|
||||||
msg`We encountered an unknown error while attempting to delete your account. Please try again later.`,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -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,18 +22,21 @@ 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();
|
||||||
|
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
|
||||||
const { mutateAsync: disableUser, isLoading: isDisablingUser } =
|
const { mutateAsync: disableUser, isPending: isDisablingUser } =
|
||||||
trpc.admin.disableUser.useMutation();
|
trpc.admin.disableUser.useMutation();
|
||||||
|
|
||||||
const onDisableAccount = async () => {
|
const onDisableAccount = async () => {
|
||||||
@ -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,18 +22,18 @@ 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();
|
||||||
|
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
|
||||||
const { mutateAsync: enableUser, isLoading: isEnablingUser } =
|
const { mutateAsync: enableUser, isPending: isEnablingUser } =
|
||||||
trpc.admin.enableUser.useMutation();
|
trpc.admin.enableUser.useMutation();
|
||||||
|
|
||||||
const onEnableAccount = async () => {
|
const onEnableAccount = async () => {
|
||||||
@ -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();
|
||||||
@ -51,9 +50,8 @@ export const DeleteDocumentDialog = ({
|
|||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
|
const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
|
||||||
|
|
||||||
const { mutateAsync: deleteDocument, isLoading } = 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,26 +81,13 @@ 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));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
|
<Dialog open={open} onOpenChange={(value) => !isPending && onOpenChange(value)}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
@ -193,8 +188,8 @@ export const DeleteDocumentDialog = ({
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
loading={isLoading}
|
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();
|
||||||
documentId: id,
|
|
||||||
});
|
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery(
|
||||||
|
{
|
||||||
|
documentId: id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: open === true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const documentData = document?.documentData
|
const documentData = document?.documentData
|
||||||
? {
|
? {
|
||||||
@ -48,17 +54,16 @@ export const DuplicateDocumentDialog = ({
|
|||||||
|
|
||||||
const documentsPath = formatDocumentsPath(team?.url);
|
const documentsPath = formatDocumentsPath(team?.url);
|
||||||
|
|
||||||
const { mutateAsync: duplicateDocument, isLoading: isDuplicateLoading } =
|
const { mutateAsync: duplicateDocument, isPending: isDuplicateLoading } =
|
||||||
trpcReact.document.duplicateDocument.useMutation({
|
trpcReact.document.duplicateDocument.useMutation({
|
||||||
onSuccess: (newId) => {
|
onSuccess: async ({ documentId }) => {
|
||||||
router.push(`${documentsPath}/${newId}/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, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
|
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
|
||||||
|
|
||||||
const { mutateAsync: moveDocument, isLoading } = trpc.document.moveDocumentToTeam.useMutation({
|
const { mutateAsync: moveDocument, isPending } = trpc.document.moveDocumentToTeam.useMutation({
|
||||||
onSuccess: () => {
|
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">
|
||||||
@ -119,8 +114,8 @@ export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocum
|
|||||||
<Button variant="secondary" onClick={() => onOpenChange(false)}>
|
<Button variant="secondary" onClick={() => onOpenChange(false)}>
|
||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onMove} loading={isLoading} disabled={!selectedTeamId || isLoading}>
|
<Button onClick={onMove} loading={isPending} disabled={!selectedTeamId || isPending}>
|
||||||
{isLoading ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
|
{isPending ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@ -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);
|
||||||
|
|
||||||
@ -65,7 +64,7 @@ export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePass
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: createPasskeyRegistrationOptions, isLoading } =
|
const { mutateAsync: createPasskeyRegistrationOptions, isPending } =
|
||||||
trpc.auth.createPasskeyRegistrationOptions.useMutation();
|
trpc.auth.createPasskeyRegistrationOptions.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: createPasskey } = trpc.auth.createPasskey.useMutation();
|
const { mutateAsync: createPasskey } = trpc.auth.createPasskey.useMutation();
|
||||||
@ -141,7 +140,7 @@ export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePass
|
|||||||
>
|
>
|
||||||
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
|
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
|
||||||
{trigger ?? (
|
{trigger ?? (
|
||||||
<Button variant="secondary" loading={isLoading}>
|
<Button variant="secondary" loading={isPending}>
|
||||||
<KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />
|
<KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />
|
||||||
<Trans>Add passkey</Trans>
|
<Trans>Add passkey</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
@ -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,
|
||||||
@ -116,7 +115,7 @@ export const ManagePublicTemplateDialog = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: updateTemplateSettings, isLoading: isUpdatingTemplateSettings } =
|
const { mutateAsync: updateTemplateSettings, isPending: isUpdatingTemplateSettings } =
|
||||||
trpc.template.updateTemplate.useMutation();
|
trpc.template.updateTemplate.useMutation();
|
||||||
|
|
||||||
const setTemplateToPrivate = async (templateId: number) => {
|
const setTemplateToPrivate = async (templateId: number) => {
|
||||||
@ -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();
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ export const CreateTeamCheckoutDialog = ({
|
|||||||
|
|
||||||
const { data, isLoading } = trpc.team.getTeamPrices.useQuery();
|
const { data, isLoading } = trpc.team.getTeamPrices.useQuery();
|
||||||
|
|
||||||
const { mutateAsync: createCheckout, isLoading: isCreatingCheckout } =
|
const { mutateAsync: createCheckout, isPending: isCreatingCheckout } =
|
||||||
trpc.team.createTeamPendingCheckout.useMutation({
|
trpc.team.createTeamPendingCheckout.useMutation({
|
||||||
onSuccess: (checkoutUrl) => {
|
onSuccess: (checkoutUrl) => {
|
||||||
window.open(checkoutUrl, '_blank');
|
window.open(checkoutUrl, '_blank');
|
||||||
@ -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),
|
||||||
@ -64,7 +61,7 @@ export const AddTeamEmailDialog = ({ teamId, trigger, ...props }: AddTeamEmailDi
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: createTeamEmailVerification, isLoading } =
|
const { mutateAsync: createTeamEmailVerification, isPending } =
|
||||||
trpc.team.createTeamEmailVerification.useMutation();
|
trpc.team.createTeamEmailVerification.useMutation();
|
||||||
|
|
||||||
const onFormSubmit = async ({ name, email }: TCreateTeamEmailFormSchema) => {
|
const onFormSubmit = async ({ name, email }: TCreateTeamEmailFormSchema) => {
|
||||||
@ -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) {
|
||||||
@ -120,7 +117,7 @@ export const AddTeamEmailDialog = ({ teamId, trigger, ...props }: AddTeamEmailDi
|
|||||||
>
|
>
|
||||||
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
|
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
|
||||||
{trigger ?? (
|
{trigger ?? (
|
||||||
<Button variant="outline" loading={isLoading} className="bg-background">
|
<Button variant="outline" loading={isPending} className="bg-background">
|
||||||
<Plus className="-ml-1 mr-1 h-5 w-5" />
|
<Plus className="-ml-1 mr-1 h-5 w-5" />
|
||||||
<Trans>Add email</Trans>
|
<Trans>Add email</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
@ -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,15 +40,14 @@ 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, isLoading: isDeletingTeamEmail } =
|
|
||||||
trpc.team.deleteTeamEmail.useMutation({
|
trpc.team.deleteTeamEmail.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast({
|
toast({
|
||||||
@ -69,7 +66,7 @@ export const RemoveTeamEmailDialog = ({ trigger, teamName, team }: RemoveTeamEma
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: deleteTeamEmailVerification, isLoading: isDeletingTeamEmailVerification } =
|
const { mutateAsync: deleteTeamEmailVerification, isPending: isDeletingTeamEmailVerification } =
|
||||||
trpc.team.deleteTeamEmailVerification.useMutation({
|
trpc.team.deleteTeamEmailVerification.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast({
|
toast({
|
||||||
@ -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,19 +30,19 @@ 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();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { mutateAsync: leaveTeam, isLoading: isLeavingTeam } = trpc.team.leaveTeam.useMutation({
|
const { mutateAsync: leaveTeam, isPending: isLeavingTeam } = trpc.team.leaveTeam.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Success`),
|
title: _(msg`Success`),
|
||||||
@ -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,20 +28,20 @@ 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();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { mutateAsync: deleteTeamMembers, isLoading: isDeletingTeamMember } =
|
const { mutateAsync: deleteTeamMembers, isPending: isDeletingTeamMember } =
|
||||||
trpc.team.deleteTeamMembers.useMutation({
|
trpc.team.deleteTeamMembers.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast({
|
toast({
|
||||||
@ -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(
|
||||||
<SelectItem key={role} value={role}>
|
(role) => (
|
||||||
{_(TEAM_MEMBER_ROLE_MAP[role]) ?? role}
|
<SelectItem key={role} value={role}>
|
||||||
</SelectItem>
|
{_(TEAM_MEMBER_ROLE_MAP[role]) ?? role}
|
||||||
))}
|
</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();
|
||||||
@ -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, isLoading } = 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`),
|
||||||
@ -51,7 +53,7 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
|
<Dialog open={open} onOpenChange={(value) => !isPending && onOpenChange(value)}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
@ -70,7 +72,7 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
disabled={isLoading}
|
disabled={isPending}
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
>
|
>
|
||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
@ -79,7 +81,7 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
loading={isLoading}
|
loading={isPending}
|
||||||
onClick={async () => deleteTemplate({ templateId: id })}
|
onClick={async () => deleteTemplate({ templateId: id })}
|
||||||
>
|
>
|
||||||
<Trans>Delete</Trans>
|
<Trans>Delete</Trans>
|
||||||
@ -1,17 +1,15 @@
|
|||||||
'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; Recipient: Recipient[] };
|
template: Template & { directLink?: TemplateDirectLink | null; recipients: Recipient[] };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TemplateDirectLinkDialogWrapper = ({
|
export const TemplateDirectLinkDialogWrapper = ({
|
||||||
@ -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';
|
||||||
@ -46,12 +45,10 @@ import {
|
|||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
|
||||||
|
|
||||||
type TemplateDirectLinkDialogProps = {
|
type TemplateDirectLinkDialogProps = {
|
||||||
template: Template & {
|
template: Template & {
|
||||||
directLink?: Pick<TemplateDirectLink, 'token' | 'enabled'> | null;
|
directLink?: Pick<TemplateDirectLink, 'token' | 'enabled'> | null;
|
||||||
Recipient: Recipient[];
|
recipients: Recipient[];
|
||||||
};
|
};
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
@ -67,11 +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 team = useOptionalCurrentTeam();
|
|
||||||
|
|
||||||
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);
|
||||||
@ -81,21 +76,21 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const validDirectTemplateRecipients = useMemo(
|
const validDirectTemplateRecipients = useMemo(
|
||||||
() => template.Recipient.filter((recipient) => recipient.role !== RecipientRole.CC),
|
() => template.recipients.filter((recipient) => recipient.role !== RecipientRole.CC),
|
||||||
[template.Recipient],
|
[template.recipients],
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutateAsync: createTemplateDirectLink,
|
mutateAsync: createTemplateDirectLink,
|
||||||
isLoading: 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);
|
||||||
@ -108,9 +103,11 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: toggleTemplateDirectLink, isLoading: 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`;
|
||||||
|
|
||||||
@ -131,9 +128,11 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: deleteTemplateDirectLink, isLoading: 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>
|
||||||
@ -326,7 +324,7 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Prevent creating placeholder direct template recipient if the email already exists. */}
|
{/* Prevent creating placeholder direct template recipient if the email already exists. */}
|
||||||
{!template.Recipient.some(
|
{!template.recipients.some(
|
||||||
(recipient) => recipient.email === DIRECT_TEMPLATE_RECIPIENT_EMAIL,
|
(recipient) => recipient.email === DIRECT_TEMPLATE_RECIPIENT_EMAIL,
|
||||||
) && (
|
) && (
|
||||||
<DialogFooter className="mx-auto">
|
<DialogFooter className="mx-auto">
|
||||||
@ -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, isLoading } =
|
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.`),
|
||||||
@ -55,7 +49,7 @@ export const DuplicateTemplateDialog = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
|
<Dialog open={open} onOpenChange={(value) => !isPending && onOpenChange(value)}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
@ -70,7 +64,7 @@ export const DuplicateTemplateDialog = ({
|
|||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isLoading}
|
disabled={isPending}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
>
|
>
|
||||||
@ -79,7 +73,7 @@ export const DuplicateTemplateDialog = ({
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
loading={isLoading}
|
loading={isPending}
|
||||||
onClick={async () =>
|
onClick={async () =>
|
||||||
duplicateTemplate({
|
duplicateTemplate({
|
||||||
templateId: id,
|
templateId: id,
|
||||||
@ -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, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
|
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
|
||||||
const { mutateAsync: moveTemplate, isLoading } = trpc.template.moveTemplateToTeam.useMutation({
|
|
||||||
onSuccess: () => {
|
const { mutateAsync: moveTemplate, isPending } = trpc.template.moveTemplateToTeam.useMutation({
|
||||||
router.refresh();
|
onSuccess: async () => {
|
||||||
|
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,8 +144,12 @@ 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={isLoading} disabled={!selectedTeamId || isLoading}>
|
<Button
|
||||||
{isLoading ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
|
onClick={handleOnMove}
|
||||||
|
loading={isPending}
|
||||||
|
disabled={!selectedTeamId || isPending}
|
||||||
|
>
|
||||||
|
{isPending ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@ -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);
|
||||||
@ -100,7 +98,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
|
|
||||||
const hasSignatureField = localFields.some((field) => field.type === FieldType.SIGNATURE);
|
const hasSignatureField = localFields.some((field) => field.type === FieldType.SIGNATURE);
|
||||||
|
|
||||||
const { mutateAsync: createDocumentFromDirectTemplate, isLoading: isSubmitting } =
|
const { mutateAsync: createDocumentFromDirectTemplate, isPending: isSubmitting } =
|
||||||
trpc.template.createDocumentFromDirectTemplate.useMutation();
|
trpc.template.createDocumentFromDirectTemplate.useMutation();
|
||||||
|
|
||||||
const onSignField = (payload: TSignFieldWithTokenMutationSchema) => {
|
const onSignField = (payload: TSignFieldWithTokenMutationSchema) => {
|
||||||
@ -118,7 +116,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (field.type === FieldType.SIGNATURE) {
|
if (field.type === FieldType.SIGNATURE) {
|
||||||
newField.Signature = {
|
newField.signature = {
|
||||||
id: 1,
|
id: 1,
|
||||||
created: new Date(),
|
created: new Date(),
|
||||||
recipientId: 1,
|
recipientId: 1,
|
||||||
@ -163,7 +161,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
customText: '',
|
customText: '',
|
||||||
inserted: false,
|
inserted: false,
|
||||||
signedValue: undefined,
|
signedValue: undefined,
|
||||||
Signature: undefined,
|
signature: undefined,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -496,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 = {
|
||||||
@ -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, Recipient, 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,16 +19,16 @@ 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;
|
recipient: Recipient;
|
||||||
@ -52,7 +50,7 @@ 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}
|
recipient={recipient}
|
||||||
@ -62,7 +60,7 @@ export const EmbedDocumentFields = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.INITIALS, () => (
|
.with(FieldType.INITIALS, () => (
|
||||||
<InitialsField
|
<DocumentSigningInitialsField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
@ -71,7 +69,7 @@ export const EmbedDocumentFields = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.NAME, () => (
|
.with(FieldType.NAME, () => (
|
||||||
<NameField
|
<DocumentSigningNameField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
@ -80,7 +78,7 @@ export const EmbedDocumentFields = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.DATE, () => (
|
.with(FieldType.DATE, () => (
|
||||||
<DateField
|
<DocumentSigningDateField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
@ -91,7 +89,7 @@ export const EmbedDocumentFields = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.EMAIL, () => (
|
.with(FieldType.EMAIL, () => (
|
||||||
<EmailField
|
<DocumentSigningEmailField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
@ -106,7 +104,7 @@ export const EmbedDocumentFields = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<DocumentSigningTextField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={fieldWithMeta}
|
field={fieldWithMeta}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
@ -122,7 +120,7 @@ export const EmbedDocumentFields = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NumberField
|
<DocumentSigningNumberField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={fieldWithMeta}
|
field={fieldWithMeta}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
@ -138,7 +136,7 @@ export const EmbedDocumentFields = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RadioField
|
<DocumentSigningRadioField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={fieldWithMeta}
|
field={fieldWithMeta}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
@ -154,7 +152,7 @@ export const EmbedDocumentFields = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CheckboxField
|
<DocumentSigningCheckboxField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={fieldWithMeta}
|
field={fieldWithMeta}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
@ -170,7 +168,7 @@ export const EmbedDocumentFields = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownField
|
<DocumentSigningDropdownField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={fieldWithMeta}
|
field={fieldWithMeta}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
@ -1,16 +1,15 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
import { useEffect, useLayoutEffect, 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 { DocumentMeta, Recipient, TemplateMeta } from '@prisma/client';
|
||||||
|
import { type DocumentData, type Field, FieldType } from '@prisma/client';
|
||||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||||
|
|
||||||
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
|
|
||||||
import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -22,14 +21,14 @@ 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 { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||||
import { Logo } from '~/components/branding/logo';
|
import { injectCss } from '~/utils/css-vars';
|
||||||
|
|
||||||
import { EmbedClientLoading } from '../../client-loading';
|
import { ZSignDocumentEmbedDataSchema } from '../../types/embed-document-sign-schema';
|
||||||
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 { ZSignDocumentEmbedDataSchema } from './schema';
|
import { EmbedDocumentFields } from './embed-document-fields';
|
||||||
|
|
||||||
export type EmbedSignDocumentClientPageProps = {
|
export type EmbedSignDocumentClientPageProps = {
|
||||||
token: string;
|
token: string;
|
||||||
@ -65,7 +64,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
setFullName,
|
setFullName,
|
||||||
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);
|
||||||
@ -84,7 +83,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
fields.filter((field) => field.inserted),
|
fields.filter((field) => field.inserted),
|
||||||
];
|
];
|
||||||
|
|
||||||
const { mutateAsync: completeDocumentWithToken, isLoading: isSubmitting } =
|
const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } =
|
||||||
trpc.recipient.completeDocumentWithToken.useMutation();
|
trpc.recipient.completeDocumentWithToken.useMutation();
|
||||||
|
|
||||||
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
|
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
|
||||||
@ -369,7 +368,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
{!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,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);
|
||||||
@ -61,7 +58,7 @@ export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorA
|
|||||||
const {
|
const {
|
||||||
mutateAsync: setup2FA,
|
mutateAsync: setup2FA,
|
||||||
data: setup2FAData,
|
data: setup2FAData,
|
||||||
isLoading: isSettingUp2FA,
|
isPending: isSettingUp2FA,
|
||||||
} = trpc.twoFactorAuthentication.setup.useMutation({
|
} = trpc.twoFactorAuthentication.setup.useMutation({
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast({
|
toast({
|
||||||
@ -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';
|
||||||
@ -47,7 +44,7 @@ export const ViewRecoveryCodesDialog = () => {
|
|||||||
const {
|
const {
|
||||||
data: recoveryCodes,
|
data: recoveryCodes,
|
||||||
mutate,
|
mutate,
|
||||||
isLoading,
|
isPending,
|
||||||
error,
|
error,
|
||||||
} = trpc.twoFactorAuthentication.viewRecoveryCodes.useMutation();
|
} = trpc.twoFactorAuthentication.viewRecoveryCodes.useMutation();
|
||||||
|
|
||||||
@ -121,7 +118,7 @@ export const ViewRecoveryCodesDialog = () => {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<fieldset className="flex flex-col space-y-4" disabled={isLoading}>
|
<fieldset className="flex flex-col space-y-4" disabled={isPending}>
|
||||||
<FormField
|
<FormField
|
||||||
name="token"
|
name="token"
|
||||||
control={viewRecoveryCodesForm.control}
|
control={viewRecoveryCodesForm.control}
|
||||||
@ -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(() => (
|
||||||
@ -164,7 +161,7 @@ export const ViewRecoveryCodesDialog = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
|
|
||||||
<Button type="submit" loading={isLoading}>
|
<Button type="submit" loading={isPending}>
|
||||||
<Trans>View</Trans>
|
<Trans>View</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@ -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,15 +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 { TRPCClientError } from '@documenso/trpc/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';
|
||||||
@ -40,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: {
|
||||||
@ -74,23 +70,15 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
toast({
|
||||||
toast({
|
title: _(msg`An unknown error occurred`),
|
||||||
title: _(msg`An error occurred`),
|
description: _(
|
||||||
description: err.message,
|
msg`We encountered an unknown error while attempting update your profile. Please try again later.`,
|
||||||
variant: 'destructive',
|
),
|
||||||
});
|
variant: 'destructive',
|
||||||
} else {
|
});
|
||||||
toast({
|
|
||||||
title: _(msg`An unknown error occurred`),
|
|
||||||
description: _(
|
|
||||||
msg`We encountered an unknown error while attempting to sign you In. Please try again later.`,
|
|
||||||
),
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -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) {
|
|
||||||
setIsTwoFactorAuthenticationDialogOpen(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorMessage = ERROR_MESSAGES[result.error];
|
if (error.code === 'TWO_FACTOR_MISSING_CREDENTIALS') {
|
||||||
|
setIsTwoFactorAuthenticationDialogOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (result.error === ErrorCode.UNVERIFIED_EMAIL) {
|
if (error.code === AuthenticationErrorCode.UnverifiedEmail) {
|
||||||
router.push(`/unverified-account`);
|
await navigate('/unverified-account');
|
||||||
|
|
||||||
toast({
|
|
||||||
title: _(msg`Unable to sign in`),
|
|
||||||
description: errorMessage ?? _(msg`An unknown error occurred`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Unable to sign in`),
|
title: _(msg`Unable to sign in`),
|
||||||
description: errorMessage ?? _(msg`An unknown error occurred`),
|
description: _(
|
||||||
variant: 'destructive',
|
msg`This account has not been verified. Please verify your account before signing in.`,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result?.url) {
|
const errorMessage = match(error.code)
|
||||||
throw new Error('An unknown error occurred');
|
.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));
|
||||||
|
|
||||||
window.location.href = result.url;
|
|
||||||
} catch (err) {
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`Unable to sign in`),
|
||||||
description: _(
|
description: _(errorMessage),
|
||||||
msg`We encountered an unknown error while attempting to sign you In. Please try again later.`,
|
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,20 +401,18 @@ export const SignInForm = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isPasskeyEnabled && (
|
<Button
|
||||||
<Button
|
type="button"
|
||||||
type="button"
|
size="lg"
|
||||||
size="lg"
|
variant="outline"
|
||||||
variant="outline"
|
disabled={isSubmitting}
|
||||||
disabled={isSubmitting}
|
loading={isPasskeyLoading}
|
||||||
loading={isPasskeyLoading}
|
className="bg-background text-muted-foreground border"
|
||||||
className="bg-background text-muted-foreground border"
|
onClick={onSignInWithPasskey}
|
||||||
onClick={onSignInWithPasskey}
|
>
|
||||||
>
|
{!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,19 +1,17 @@
|
|||||||
'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 { z } from 'zod';
|
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 type { ApiToken } from '@documenso/prisma/client';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { TRPCClientError } from '@documenso/trpc/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';
|
||||||
@ -40,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(),
|
||||||
@ -60,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();
|
||||||
@ -71,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);
|
||||||
@ -129,25 +125,22 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
|
|||||||
});
|
});
|
||||||
|
|
||||||
form.reset();
|
form.reset();
|
||||||
|
} catch (err) {
|
||||||
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
const errorMessage = match(error.code)
|
||||||
} catch (error) {
|
.with(
|
||||||
if (error instanceof TRPCClientError && error.data?.code === 'BAD_REQUEST') {
|
AppErrorCode.UNAUTHORIZED,
|
||||||
toast({
|
() => msg`You do not have permission to create a token for this team`,
|
||||||
title: _(msg`An error occurred`),
|
)
|
||||||
description: error.message,
|
.otherwise(() => msg`Something went wrong. Please try again later.`);
|
||||||
variant: 'destructive',
|
|
||||||
});
|
toast({
|
||||||
} else {
|
title: _(msg`An error occurred`),
|
||||||
toast({
|
description: _(errorMessage),
|
||||||
title: _(msg`An unknown error occurred`),
|
variant: 'destructive',
|
||||||
description: _(
|
duration: 5000,
|
||||||
msg`We encountered an unknown error while attempting create the new token. Please try again later.`,
|
});
|
||||||
),
|
|
||||||
variant: 'destructive',
|
|
||||||
duration: 5000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -263,34 +256,36 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
|
|||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<AnimatePresence initial={!hasNewlyCreatedToken}>
|
<AnimatePresence>
|
||||||
{newlyCreatedToken && hasNewlyCreatedToken && (
|
{newlyCreatedToken &&
|
||||||
<motion.div
|
tokens &&
|
||||||
className="mt-8"
|
tokens.find((token) => token.id === newlyCreatedToken.id) && (
|
||||||
initial={{ opacity: 0, y: -40 }}
|
<motion.div
|
||||||
animate={{ opacity: 1, y: 0 }}
|
className="mt-8"
|
||||||
exit={{ opacity: 0, y: 40 }}
|
initial={{ opacity: 0, y: -40 }}
|
||||||
>
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<Card gradient>
|
exit={{ opacity: 0, y: 40 }}
|
||||||
<CardContent className="p-4">
|
>
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
<Card gradient>
|
||||||
<Trans>
|
<CardContent className="p-4">
|
||||||
Your token was created successfully! Make sure to copy it because you won't be
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
able to see it again!
|
<Trans>
|
||||||
</Trans>
|
Your token was created successfully! Make sure to copy it because you won't be
|
||||||
</p>
|
able to see it again!
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p className="bg-muted-foreground/10 my-4 rounded-md px-2.5 py-1 font-mono text-sm">
|
<p className="bg-muted-foreground/10 my-4 rounded-md px-2.5 py-1 font-mono text-sm">
|
||||||
{newlyCreatedToken.token}
|
{newlyCreatedToken.token}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button variant="outline" onClick={() => void copyToken(newlyCreatedToken.token)}>
|
<Button variant="outline" onClick={() => void copyToken(newlyCreatedToken.token)}>
|
||||||
<Trans>Copy token</Trans>
|
<Trans>Copy token</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -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;
|
||||||
@ -23,7 +21,7 @@ const CustomTooltip = ({
|
|||||||
}: TooltipProps<ValueType, NameType> & { tooltip?: string }) => {
|
}: TooltipProps<ValueType, NameType> & { tooltip?: string }) => {
|
||||||
if (active && payload && payload.length) {
|
if (active && payload && payload.length) {
|
||||||
return (
|
return (
|
||||||
<div className="z-100 w-60 space-y-1 rounded-md border border-solid bg-white p-2 px-3">
|
<div className="z-100 w-60 space-y-1 rounded-md border border-solid bg-white p-2 px-3">
|
||||||
<p className="">{label}</p>
|
<p className="">{label}</p>
|
||||||
<p className="text-documenso">
|
<p className="text-documenso">
|
||||||
{`${tooltip} : `}
|
{`${tooltip} : `}
|
||||||
@ -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,16 +66,15 @@ 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('');
|
||||||
@ -91,7 +86,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
|||||||
query: search,
|
query: search,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keepPreviousData: true,
|
placeholderData: (previousData) => previousData,
|
||||||
// Do not batch this due to relatively long request time compared to
|
// Do not batch this due to relatively long request time compared to
|
||||||
// other queries which are generally batched with this.
|
// other queries which are generally batched with this.
|
||||||
...SKIP_QUERY_BATCH_META,
|
...SKIP_QUERY_BATCH_META,
|
||||||
@ -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',
|
||||||
{
|
{
|
||||||
@ -82,7 +85,7 @@ export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: Deskto
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-muted-foreground bg-muted flex items-center rounded-md px-1.5 py-0.5 text-xs tracking-wider">
|
<div className="text-muted-foreground bg-muted flex items-center rounded-md px-1.5 py-0.5 text-xs tracking-wider">
|
||||||
{modifierKey}+K
|
{modifierKey}+K
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -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,14 +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 type { Field, Recipient } from '@documenso/prisma/client';
|
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import type { TemplateWithDetails } from '@documenso/prisma/types/template';
|
import type { TTemplate } from '@documenso/lib/types/template';
|
||||||
import {
|
import {
|
||||||
DocumentFlowFormContainerActions,
|
DocumentFlowFormContainerActions,
|
||||||
DocumentFlowFormContainerContent,
|
DocumentFlowFormContainerContent,
|
||||||
@ -29,38 +29,39 @@ 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<TemplateWithDetails, 'User'>;
|
template: Omit<TTemplate, 'user'>;
|
||||||
directTemplateRecipient: Recipient & { Field: 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 { Recipient } = template;
|
const { user } = useOptionalSession();
|
||||||
const { derivedRecipientAccessAuth } = useRequiredDocumentAuthContext();
|
|
||||||
|
|
||||||
const recipientsWithBlankDirectRecipientEmail = Recipient.map((recipient) => {
|
const { recipients } = template;
|
||||||
|
const { derivedRecipientAccessAuth } = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
|
const recipientsWithBlankDirectRecipientEmail = recipients.map((recipient) => {
|
||||||
if (recipient.id === directTemplateRecipient.id) {
|
if (recipient.id === directTemplateRecipient.id) {
|
||||||
return {
|
return {
|
||||||
...recipient,
|
...recipient,
|
||||||
@ -71,10 +72,10 @@ 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.Recipient.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,
|
||||||
message: _(msg`Email cannot already exist in the template`),
|
message: _(msg`Email cannot already exist in the template`),
|
||||||
@ -96,7 +97,7 @@ export const ConfigureDirectTemplateFormPartial = ({
|
|||||||
|
|
||||||
<DocumentFlowFormContainerContent>
|
<DocumentFlowFormContainerContent>
|
||||||
{isDocumentPdfLoaded &&
|
{isDocumentPdfLoaded &&
|
||||||
directTemplateRecipient.Field.map((field, index) => (
|
directTemplateRecipient.fields.map((field, index) => (
|
||||||
<ShowFieldItem
|
<ShowFieldItem
|
||||||
key={index}
|
key={index}
|
||||||
field={field}
|
field={field}
|
||||||
@ -124,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 { Field } from '@documenso/prisma/client';
|
import type { TTemplate } from '@documenso/lib/types/template';
|
||||||
import { type Recipient } from '@documenso/prisma/client';
|
|
||||||
import type { TemplateWithDetails } from '@documenso/prisma/types/template';
|
|
||||||
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,18 +16,22 @@ 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<TemplateWithDetails, 'User'>;
|
template: Omit<TTemplate, 'user'>;
|
||||||
directTemplateToken: string;
|
directTemplateToken: string;
|
||||||
directTemplateRecipient: Recipient & { Field: Field[] };
|
directTemplateRecipient: Recipient & { fields: Field[] };
|
||||||
};
|
};
|
||||||
|
|
||||||
type DirectTemplateStep = 'configure' | 'sign';
|
type DirectTemplateStep = 'configure' | 'sign';
|
||||||
@ -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,10 +162,10 @@ export const DirectTemplatePageView = ({
|
|||||||
initialEmail={email}
|
initialEmail={email}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SignDirectTemplateForm
|
<DirectTemplateSigningForm
|
||||||
flowStep={directTemplateFlow.sign}
|
flowStep={directTemplateFlow.sign}
|
||||||
directRecipient={recipient}
|
directRecipient={recipient}
|
||||||
directRecipientFields={directTemplateRecipient.Field}
|
directRecipientFields={directTemplateRecipient.fields}
|
||||||
template={template}
|
template={template}
|
||||||
onSubmit={onSignDirectTemplateSubmit}
|
onSubmit={onSignDirectTemplateSubmit}
|
||||||
/>
|
/>
|
||||||
@ -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';
|
||||||
|
|
||||||
@ -14,10 +16,8 @@ import {
|
|||||||
ZRadioFieldMeta,
|
ZRadioFieldMeta,
|
||||||
ZTextFieldMeta,
|
ZTextFieldMeta,
|
||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
|
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 { TemplateWithDetails } from '@documenso/prisma/types/template';
|
|
||||||
import type {
|
import type {
|
||||||
TRemovedSignedFieldWithTokenMutationSchema,
|
TRemovedSignedFieldWithTokenMutationSchema,
|
||||||
TSignFieldWithTokenMutationSchema,
|
TSignFieldWithTokenMutationSchema,
|
||||||
@ -38,41 +38,41 @@ 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 { SignDialog } from '~/app/(signing)/sign/[token]/sign-dialog';
|
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 SignDirectTemplateFormProps = {
|
export type DirectTemplateSigningFormProps = {
|
||||||
flowStep: DocumentFlowStep;
|
flowStep: DocumentFlowStep;
|
||||||
directRecipient: Recipient;
|
directRecipient: Recipient;
|
||||||
directRecipientFields: Field[];
|
directRecipientFields: Field[];
|
||||||
template: Omit<TemplateWithDetails, 'User'>;
|
template: Omit<TTemplate, 'user'>;
|
||||||
onSubmit: (_data: DirectTemplateLocalField[]) => Promise<void>;
|
onSubmit: (_data: DirectTemplateLocalField[]) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DirectTemplateLocalField = Field & {
|
export type DirectTemplateLocalField = Field & {
|
||||||
signedValue?: TSignFieldWithTokenMutationSchema;
|
signedValue?: TSignFieldWithTokenMutationSchema;
|
||||||
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);
|
||||||
@ -95,7 +95,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (field.type === FieldType.SIGNATURE) {
|
if (field.type === FieldType.SIGNATURE) {
|
||||||
tempField.Signature = {
|
tempField.signature = {
|
||||||
id: 1,
|
id: 1,
|
||||||
created: new Date(),
|
created: new Date(),
|
||||||
recipientId: 1,
|
recipientId: 1,
|
||||||
@ -127,7 +127,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
customText: '',
|
customText: '',
|
||||||
inserted: false,
|
inserted: false,
|
||||||
signedValue: undefined,
|
signedValue: undefined,
|
||||||
Signature: undefined,
|
signature: undefined,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -183,7 +183,7 @@ 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}
|
recipient={directRecipient}
|
||||||
@ -192,7 +192,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.INITIALS, () => (
|
.with(FieldType.INITIALS, () => (
|
||||||
<InitialsField
|
<DocumentSigningInitialsField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={directRecipient}
|
recipient={directRecipient}
|
||||||
@ -201,7 +201,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.NAME, () => (
|
.with(FieldType.NAME, () => (
|
||||||
<NameField
|
<DocumentSigningNameField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={directRecipient}
|
recipient={directRecipient}
|
||||||
@ -210,7 +210,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.DATE, () => (
|
.with(FieldType.DATE, () => (
|
||||||
<DateField
|
<DocumentSigningDateField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={directRecipient}
|
recipient={directRecipient}
|
||||||
@ -221,7 +221,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.EMAIL, () => (
|
.with(FieldType.EMAIL, () => (
|
||||||
<EmailField
|
<DocumentSigningEmailField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={directRecipient}
|
recipient={directRecipient}
|
||||||
@ -235,7 +235,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<DocumentSigningTextField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={{
|
field={{
|
||||||
...field,
|
...field,
|
||||||
@ -253,7 +253,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NumberField
|
<DocumentSigningNumberField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={{
|
field={{
|
||||||
...field,
|
...field,
|
||||||
@ -271,7 +271,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownField
|
<DocumentSigningDropdownField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={{
|
field={{
|
||||||
...field,
|
...field,
|
||||||
@ -289,7 +289,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RadioField
|
<DocumentSigningRadioField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={{
|
field={{
|
||||||
...field,
|
...field,
|
||||||
@ -307,7 +307,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CheckboxField
|
<DocumentSigningCheckboxField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={{
|
field={{
|
||||||
...field,
|
...field,
|
||||||
@ -373,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}
|
||||||
@ -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 using email password.
|
||||||
every time you sign in.
|
</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);
|
||||||
|
|
||||||
@ -106,7 +99,7 @@ export const DocumentAuthProvider = ({
|
|||||||
perPage: MAXIMUM_PASSKEYS,
|
perPage: MAXIMUM_PASSKEYS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keepPreviousData: true,
|
placeholderData: (previousData) => previousData,
|
||||||
enabled: derivedRecipientActionAuth === DocumentAuth.PASSKEY,
|
enabled: derivedRecipientActionAuth === DocumentAuth.PASSKEY,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -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,19 +1,16 @@
|
|||||||
'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';
|
||||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { ZCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
|
import { ZCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
|
||||||
import { fromCheckboxValue, toCheckboxValue } from '@documenso/lib/universal/field-checkbox';
|
import { fromCheckboxValue, toCheckboxValue } from '@documenso/lib/universal/field-checkbox';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
|
||||||
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type {
|
import type {
|
||||||
@ -26,28 +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 { SigningFieldContainer } from './signing-field-container';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
|
|
||||||
export type CheckboxFieldProps = {
|
export type DocumentSigningCheckboxFieldProps = {
|
||||||
field: FieldWithSignatureAndFieldMeta;
|
field: FieldWithSignatureAndFieldMeta;
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CheckboxField = ({
|
export const DocumentSigningCheckboxField = ({
|
||||||
field,
|
field,
|
||||||
recipient,
|
recipient,
|
||||||
onSignField,
|
onSignField,
|
||||||
onUnsignField,
|
onUnsignField,
|
||||||
}: CheckboxFieldProps) => {
|
}: DocumentSigningCheckboxFieldProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
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);
|
||||||
|
|
||||||
@ -81,15 +77,15 @@ export const CheckboxField = ({
|
|||||||
);
|
);
|
||||||
}, [checkedValues, validationSign, checkboxValidationLength]);
|
}, [checkedValues, validationSign, checkboxValidationLength]);
|
||||||
|
|
||||||
const { mutateAsync: signFieldWithToken, isLoading: 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);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutateAsync: removeSignedFieldWithToken,
|
mutateAsync: removeSignedFieldWithToken,
|
||||||
isLoading: 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);
|
||||||
@ -110,7 +106,7 @@ export const CheckboxField = ({
|
|||||||
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);
|
||||||
|
|
||||||
@ -145,7 +141,7 @@ export const CheckboxField = ({
|
|||||||
setCheckedValues([]);
|
setCheckedValues([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
@ -217,7 +213,7 @@ export const CheckboxField = ({
|
|||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setCheckedValues(updatedValues);
|
setCheckedValues(updatedValues);
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -236,7 +232,12 @@ export const CheckboxField = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
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" />
|
||||||
@ -294,6 +295,6 @@ export const CheckboxField = ({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</SigningFieldContainer>
|
</DocumentSigningFieldContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1,10 +1,10 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { Trans } from '@lingui/macro';
|
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 { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||||
import type { Field } from '@documenso/prisma/client';
|
|
||||||
import { RecipientRole } from '@documenso/prisma/client';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -14,9 +14,9 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
|
||||||
import { SigningDisclosure } from '~/components/general/signing-disclosure';
|
import { DocumentSigningDisclosure } from '~/components/general/document-signing/document-signing-disclosure';
|
||||||
|
|
||||||
export type SignDialogProps = {
|
export type DocumentSigningCompleteDialogProps = {
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
documentTitle: string;
|
documentTitle: string;
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
@ -26,7 +26,7 @@ export type SignDialogProps = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SignDialog = ({
|
export const DocumentSigningCompleteDialog = ({
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
documentTitle,
|
documentTitle,
|
||||||
fields,
|
fields,
|
||||||
@ -34,7 +34,7 @@ export const SignDialog = ({
|
|||||||
onSignatureComplete,
|
onSignatureComplete,
|
||||||
role,
|
role,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}: SignDialogProps) => {
|
}: DocumentSigningCompleteDialogProps) => {
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
|
|
||||||
const isComplete = useMemo(() => !fieldsContainUnsignedRequiredField(fields), [fields]);
|
const isComplete = useMemo(() => !fieldsContainUnsignedRequiredField(fields), [fields]);
|
||||||
@ -116,7 +116,7 @@ export const SignDialog = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SigningDisclosure className="mt-4" />
|
<DocumentSigningDisclosure className="mt-4" />
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
||||||
@ -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,7 +13,6 @@ 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 type { Recipient } from '@documenso/prisma/client';
|
|
||||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type {
|
import type {
|
||||||
@ -25,9 +21,9 @@ import type {
|
|||||||
} from '@documenso/trpc/server/field-router/schema';
|
} from '@documenso/trpc/server/field-router/schema';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { SigningFieldContainer } from './signing-field-container';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
|
|
||||||
export type DateFieldProps = {
|
export type DocumentSigningDateFieldProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
dateFormat?: string | null;
|
dateFormat?: string | null;
|
||||||
@ -36,37 +32,34 @@ export type DateFieldProps = {
|
|||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DateField = ({
|
export const DocumentSigningDateField = ({
|
||||||
field,
|
field,
|
||||||
recipient,
|
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 [isPending, startTransition] = useTransition();
|
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
||||||
|
|
||||||
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } =
|
|
||||||
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutateAsync: removeSignedFieldWithToken,
|
mutateAsync: removeSignedFieldWithToken,
|
||||||
isLoading: 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 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) => {
|
||||||
@ -85,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);
|
||||||
|
|
||||||
@ -117,7 +110,7 @@ export const DateField = ({
|
|||||||
|
|
||||||
await removeSignedFieldWithToken(payload);
|
await removeSignedFieldWithToken(payload);
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
@ -130,7 +123,7 @@ export const DateField = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigningFieldContainer
|
<DocumentSigningFieldContainer
|
||||||
field={field}
|
field={field}
|
||||||
onSign={onSign}
|
onSign={onSign}
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
@ -154,6 +147,6 @@ export const DateField = ({
|
|||||||
{localDateString}
|
{localDateString}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user