mirror of
https://github.com/documenso/documenso.git
synced 2025-11-22 12:41:36 +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: {
|
||||
'@next/next/no-img-element': 'off',
|
||||
'no-unreachable': 'error',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
},
|
||||
settings: {
|
||||
next: {
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -10,7 +10,7 @@ on:
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
2
.github/workflows/e2e-tests.yml
vendored
2
.github/workflows/e2e-tests.yml
vendored
@ -8,7 +8,7 @@ jobs:
|
||||
e2e_tests:
|
||||
name: 'E2E Tests'
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: warp-ubuntu-2204-x64-16x
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"@documenso/tailwind-config": "*",
|
||||
"@documenso/trpc": "*",
|
||||
"@documenso/ui": "*",
|
||||
"next": "14.2.23",
|
||||
"next": "14.2.6",
|
||||
"next-plausible": "^3.12.0",
|
||||
"nextra": "^2.13.4",
|
||||
"nextra-theme-docs": "^2.13.4",
|
||||
@ -27,6 +27,6 @@
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"typescript": "5.2.2"
|
||||
"typescript": "5.6.2"
|
||||
}
|
||||
}
|
||||
@ -5,5 +5,6 @@
|
||||
"svelte": "Svelte Integration",
|
||||
"solid": "Solid Integration",
|
||||
"preact": "Preact Integration",
|
||||
"angular": "Angular Integration",
|
||||
"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
|
||||
|
||||
Our embedding feature lets you integrate our document signing experience into your own application or website. Whether you're building with React, Preact, Vue, Svelte, Solid, or using generalized web components, this guide will help you get started with embedding Documenso.
|
||||
Our embedding feature lets you integrate our document signing experience into your own application or website. Whether you're building with React, Preact, Vue, Svelte, Solid, Angular, or using generalized web components, this guide will help you get started with embedding Documenso.
|
||||
|
||||
## Availability
|
||||
|
||||
@ -73,13 +73,14 @@ These customization options are available for both Direct Templates and Signing
|
||||
|
||||
We support embedding across a range of popular JavaScript frameworks, including:
|
||||
|
||||
| Framework | Package |
|
||||
| --------- | -------------------------------------------------------------------------------- |
|
||||
| React | [@documenso/embed-react](https://www.npmjs.com/package/@documenso/embed-react) |
|
||||
| Preact | [@documenso/embed-preact](https://www.npmjs.com/package/@documenso/embed-preact) |
|
||||
| Vue | [@documenso/embed-vue](https://www.npmjs.com/package/@documenso/embed-vue) |
|
||||
| Svelte | [@documenso/embed-svelte](https://www.npmjs.com/package/@documenso/embed-svelte) |
|
||||
| Solid | [@documenso/embed-solid](https://www.npmjs.com/package/@documenso/embed-solid) |
|
||||
| Framework | Package |
|
||||
| --------- | ---------------------------------------------------------------------------------- |
|
||||
| React | [@documenso/embed-react](https://www.npmjs.com/package/@documenso/embed-react) |
|
||||
| Preact | [@documenso/embed-preact](https://www.npmjs.com/package/@documenso/embed-preact) |
|
||||
| Vue | [@documenso/embed-vue](https://www.npmjs.com/package/@documenso/embed-vue) |
|
||||
| Svelte | [@documenso/embed-svelte](https://www.npmjs.com/package/@documenso/embed-svelte) |
|
||||
| Solid | [@documenso/embed-solid](https://www.npmjs.com/package/@documenso/embed-solid) |
|
||||
| Angular | [@documenso/embed-angular](https://www.npmjs.com/package/@documenso/embed-angular) |
|
||||
|
||||
Additionally, we provide **web components** for more generalized use. However, please note that web components are still in their early stages and haven't been extensively tested.
|
||||
|
||||
@ -127,7 +128,7 @@ This will show a dialog which will ask you to configure which recipient should b
|
||||
|
||||
## Embedding with Signing Tokens
|
||||
|
||||
To embed the signing process for an ordinary document, 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
|
||||
|
||||
@ -164,6 +165,7 @@ Once you've obtained the appropriate tokens, you can integrate the signing exper
|
||||
- [Vue](/developers/embedding/vue)
|
||||
- [Svelte](/developers/embedding/svelte)
|
||||
- [Solid](/developers/embedding/solid)
|
||||
- [Angular](/developers/embedding/angular)
|
||||
|
||||
If you're using **web components**, the integration process is slightly different. Keep in mind that web components are currently less tested but can still provide flexibility for general use cases.
|
||||
|
||||
@ -174,4 +176,5 @@ If you're using **web components**, the integration process is slightly differen
|
||||
- [Svelte Integration](/developers/embedding/svelte)
|
||||
- [Solid Integration](/developers/embedding/solid)
|
||||
- [Preact Integration](/developers/embedding/preact)
|
||||
- [Angular Integration](/developers/embedding/angular)
|
||||
- [CSS Variables](/developers/embedding/css-variables)
|
||||
|
||||
@ -13,11 +13,11 @@
|
||||
"dependencies": {
|
||||
"@documenso/prisma": "*",
|
||||
"luxon": "^3.5.0",
|
||||
"next": "14.2.23"
|
||||
"next": "14.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"typescript": "5.2.2"
|
||||
"@types/react": "18.3.5",
|
||||
"typescript": "5.6.2"
|
||||
}
|
||||
}
|
||||
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 { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
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 { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type DeleteAccountDialogProps = {
|
||||
export type AccountDeleteDialogProps = {
|
||||
className?: string;
|
||||
user: User;
|
||||
};
|
||||
|
||||
export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProps) => {
|
||||
export const AccountDeleteDialog = ({ className }: AccountDeleteDialogProps) => {
|
||||
const { user } = useSession();
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
@ -36,7 +36,7 @@ export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProp
|
||||
|
||||
const [enteredEmail, setEnteredEmail] = useState<string>('');
|
||||
|
||||
const { mutateAsync: deleteAccount, isLoading: isDeletingAccount } =
|
||||
const { mutateAsync: deleteAccount, isPending: isDeletingAccount } =
|
||||
trpc.profile.deleteAccount.useMutation();
|
||||
|
||||
const onDeleteAccount = async () => {
|
||||
@ -49,7 +49,7 @@ export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProp
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
return await signOut({ callbackUrl: '/' });
|
||||
return await authClient.signOut();
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
@ -118,7 +118,7 @@ export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProp
|
||||
</DialogHeader>
|
||||
|
||||
{!hasTwoFactorAuthentication && (
|
||||
<div className="mt-4">
|
||||
<div>
|
||||
<Label>
|
||||
<Trans>
|
||||
Please type{' '}
|
||||
@ -1,14 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -24,19 +21,19 @@ import {
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type SuperDeleteDocumentDialogProps = {
|
||||
export type AdminDocumentDeleteDialogProps = {
|
||||
document: Document;
|
||||
};
|
||||
|
||||
export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialogProps) => {
|
||||
export const AdminDocumentDeleteDialog = ({ document }: AdminDocumentDeleteDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [reason, setReason] = useState('');
|
||||
|
||||
const { mutateAsync: deleteDocument, isLoading: isDeletingDocument } =
|
||||
const { mutateAsync: deleteDocument, isPending: isDeletingDocument } =
|
||||
trpc.admin.deleteDocument.useMutation();
|
||||
|
||||
const handleDeleteDocument = async () => {
|
||||
@ -53,23 +50,14 @@ export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialo
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
router.push('/admin/documents');
|
||||
await navigate('/admin/documents');
|
||||
} catch (err) {
|
||||
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
||||
toast({
|
||||
title: _(msg`An error occurred`),
|
||||
description: err.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
variant: 'destructive',
|
||||
description:
|
||||
err.message ??
|
||||
'We encountered an unknown error while attempting to delete your document. Please try again later.',
|
||||
});
|
||||
}
|
||||
toast({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
variant: 'destructive',
|
||||
description:
|
||||
'We encountered an unknown error while attempting to delete your document. Please try again later.',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -77,7 +65,7 @@ export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialo
|
||||
<div>
|
||||
<div>
|
||||
<Alert
|
||||
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row "
|
||||
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
|
||||
variant="neutral"
|
||||
>
|
||||
<div>
|
||||
@ -1,14 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 { TRPCClientError } from '@documenso/trpc/client';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -24,20 +23,18 @@ import {
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type DeleteUserDialogProps = {
|
||||
export type AdminUserDeleteDialogProps = {
|
||||
className?: string;
|
||||
user: User;
|
||||
};
|
||||
|
||||
export const DeleteUserDialog = ({ className, user }: DeleteUserDialogProps) => {
|
||||
export const AdminUserDeleteDialog = ({ className, user }: AdminUserDeleteDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const { mutateAsync: deleteUser, isLoading: isDeletingUser } =
|
||||
const { mutateAsync: deleteUser, isPending: isDeletingUser } =
|
||||
trpc.admin.deleteUser.useMutation();
|
||||
|
||||
const onDeleteAccount = async () => {
|
||||
@ -46,31 +43,27 @@ export const DeleteUserDialog = ({ className, user }: DeleteUserDialogProps) =>
|
||||
id: user.id,
|
||||
});
|
||||
|
||||
await navigate('/admin/users');
|
||||
|
||||
toast({
|
||||
title: _(msg`Account deleted`),
|
||||
description: _(msg`The account has been deleted successfully.`),
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
router.push('/admin/users');
|
||||
} catch (err) {
|
||||
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
||||
toast({
|
||||
title: _(msg`An error occurred`),
|
||||
description: err.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
variant: 'destructive',
|
||||
description:
|
||||
err.message ??
|
||||
_(
|
||||
msg`We encountered an unknown error while attempting to delete your account. Please try again later.`,
|
||||
),
|
||||
});
|
||||
}
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
const errorMessage = match(error.code)
|
||||
.with(AppErrorCode.NOT_FOUND, () => msg`User not found.`)
|
||||
.with(AppErrorCode.UNAUTHORIZED, () => msg`You are not authorized to delete this user.`)
|
||||
.otherwise(() => msg`An error occurred while deleting the user.`);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(errorMessage),
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { User } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -23,18 +22,21 @@ import {
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type DisableUserDialogProps = {
|
||||
export type AdminUserDisableDialogProps = {
|
||||
className?: string;
|
||||
userToDisable: User;
|
||||
};
|
||||
|
||||
export const DisableUserDialog = ({ className, userToDisable }: DisableUserDialogProps) => {
|
||||
export const AdminUserDisableDialog = ({
|
||||
className,
|
||||
userToDisable,
|
||||
}: AdminUserDisableDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const { mutateAsync: disableUser, isLoading: isDisablingUser } =
|
||||
const { mutateAsync: disableUser, isPending: isDisablingUser } =
|
||||
trpc.admin.disableUser.useMutation();
|
||||
|
||||
const onDisableAccount = async () => {
|
||||
@ -1,13 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { User } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -23,18 +22,18 @@ import {
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type EnableUserDialogProps = {
|
||||
export type AdminUserEnableDialogProps = {
|
||||
className?: string;
|
||||
userToEnable: User;
|
||||
};
|
||||
|
||||
export const EnableUserDialog = ({ className, userToEnable }: EnableUserDialogProps) => {
|
||||
export const AdminUserEnableDialog = ({ className, userToEnable }: AdminUserEnableDialogProps) => {
|
||||
const { toast } = useToast();
|
||||
const { _ } = useLingui();
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const { mutateAsync: enableUser, isLoading: isEnablingUser } =
|
||||
const { mutateAsync: enableUser, isPending: isEnablingUser } =
|
||||
trpc.admin.enableUser.useMutation();
|
||||
|
||||
const onEnableAccount = async () => {
|
||||
@ -1,13 +1,12 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
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 { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -22,26 +21,26 @@ import {
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
type DeleteDocumentDialogProps = {
|
||||
type DocumentDeleteDialogProps = {
|
||||
id: number;
|
||||
open: boolean;
|
||||
onOpenChange: (_open: boolean) => void;
|
||||
onDelete?: () => Promise<void> | void;
|
||||
status: DocumentStatus;
|
||||
documentTitle: string;
|
||||
teamId?: number;
|
||||
canManageDocument: boolean;
|
||||
};
|
||||
|
||||
export const DeleteDocumentDialog = ({
|
||||
export const DocumentDeleteDialog = ({
|
||||
id,
|
||||
open,
|
||||
onOpenChange,
|
||||
onDelete,
|
||||
status,
|
||||
documentTitle,
|
||||
canManageDocument,
|
||||
}: DeleteDocumentDialogProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
}: DocumentDeleteDialogProps) => {
|
||||
const { toast } = useToast();
|
||||
const { refreshLimits } = useLimits();
|
||||
const { _ } = useLingui();
|
||||
@ -51,9 +50,8 @@ export const DeleteDocumentDialog = ({
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
|
||||
|
||||
const { mutateAsync: deleteDocument, isLoading } = trpcReact.document.deleteDocument.useMutation({
|
||||
onSuccess: () => {
|
||||
router.refresh();
|
||||
const { mutateAsync: deleteDocument, isPending } = trpcReact.document.deleteDocument.useMutation({
|
||||
onSuccess: async () => {
|
||||
void refreshLimits();
|
||||
|
||||
toast({
|
||||
@ -62,8 +60,18 @@ export const DeleteDocumentDialog = ({
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
await onDelete?.();
|
||||
|
||||
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(() => {
|
||||
@ -73,26 +81,13 @@ export const DeleteDocumentDialog = ({
|
||||
}
|
||||
}, [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>) => {
|
||||
setInputValue(event.target.value);
|
||||
setIsDeleteEnabled(event.target.value === _(deleteMessage));
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
|
||||
<Dialog open={open} onOpenChange={(value) => !isPending && onOpenChange(value)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
@ -193,8 +188,8 @@ export const DeleteDocumentDialog = ({
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
loading={isLoading}
|
||||
onClick={onDelete}
|
||||
loading={isPending}
|
||||
onClick={() => void deleteDocument({ documentId: id })}
|
||||
disabled={!isDeleteEnabled && canManageDocument}
|
||||
variant="destructive"
|
||||
>
|
||||
@ -1,10 +1,9 @@
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import type { Team } from '@documenso/prisma/client';
|
||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
@ -17,27 +16,34 @@ import {
|
||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
type DuplicateDocumentDialogProps = {
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
type DocumentDuplicateDialogProps = {
|
||||
id: number;
|
||||
open: boolean;
|
||||
onOpenChange: (_open: boolean) => void;
|
||||
team?: Pick<Team, 'id' | 'url'>;
|
||||
};
|
||||
|
||||
export const DuplicateDocumentDialog = ({
|
||||
export const DocumentDuplicateDialog = ({
|
||||
id,
|
||||
open,
|
||||
onOpenChange,
|
||||
team,
|
||||
}: DuplicateDocumentDialogProps) => {
|
||||
const router = useRouter();
|
||||
}: DocumentDuplicateDialogProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { toast } = useToast();
|
||||
const { _ } = useLingui();
|
||||
|
||||
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({
|
||||
documentId: id,
|
||||
});
|
||||
const team = useOptionalCurrentTeam();
|
||||
|
||||
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery(
|
||||
{
|
||||
documentId: id,
|
||||
},
|
||||
{
|
||||
enabled: open === true,
|
||||
},
|
||||
);
|
||||
|
||||
const documentData = document?.documentData
|
||||
? {
|
||||
@ -48,17 +54,16 @@ export const DuplicateDocumentDialog = ({
|
||||
|
||||
const documentsPath = formatDocumentsPath(team?.url);
|
||||
|
||||
const { mutateAsync: duplicateDocument, isLoading: isDuplicateLoading } =
|
||||
const { mutateAsync: duplicateDocument, isPending: isDuplicateLoading } =
|
||||
trpcReact.document.duplicateDocument.useMutation({
|
||||
onSuccess: (newId) => {
|
||||
router.push(`${documentsPath}/${newId}/edit`);
|
||||
|
||||
onSuccess: async ({ documentId }) => {
|
||||
toast({
|
||||
title: _(msg`Document Duplicated`),
|
||||
description: _(msg`Your document has been successfully duplicated.`),
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
await navigate(`${documentsPath}/${documentId}/edit`);
|
||||
onOpenChange(false);
|
||||
},
|
||||
});
|
||||
@ -1,11 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -26,30 +25,28 @@ import {
|
||||
} from '@documenso/ui/primitives/select';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
type MoveDocumentDialogProps = {
|
||||
type DocumentMoveDialogProps = {
|
||||
documentId: number;
|
||||
open: boolean;
|
||||
onOpenChange: (_open: boolean) => void;
|
||||
};
|
||||
|
||||
export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocumentDialogProps) => {
|
||||
export const DocumentMoveDialog = ({ documentId, open, onOpenChange }: DocumentMoveDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
|
||||
|
||||
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
|
||||
|
||||
const { mutateAsync: moveDocument, isLoading } = trpc.document.moveDocumentToTeam.useMutation({
|
||||
const { mutateAsync: moveDocument, isPending } = trpc.document.moveDocumentToTeam.useMutation({
|
||||
onSuccess: () => {
|
||||
router.refresh();
|
||||
toast({
|
||||
title: _(msg`Document moved`),
|
||||
description: _(msg`The document has been successfully moved to the selected team.`),
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
onOpenChange(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
@ -97,9 +94,7 @@ export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocum
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="h-8 w-8">
|
||||
{team.avatarImageId && (
|
||||
<AvatarImage
|
||||
src={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${team.avatarImageId}`}
|
||||
/>
|
||||
<AvatarImage src={formatAvatarUrl(team.avatarImageId)} />
|
||||
)}
|
||||
|
||||
<AvatarFallback className="text-sm text-gray-400">
|
||||
@ -119,8 +114,8 @@ export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocum
|
||||
<Button variant="secondary" onClick={() => onOpenChange(false)}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
<Button onClick={onMove} loading={isLoading} disabled={!selectedTeamId || isLoading}>
|
||||
{isLoading ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
|
||||
<Button onClick={onMove} loading={isPending} disabled={!selectedTeamId || isPending}>
|
||||
{isPending ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@ -1,19 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 { useSession } from 'next-auth/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
||||
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 { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -37,16 +36,17 @@ import {
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
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';
|
||||
|
||||
export type ResendDocumentActionItemProps = {
|
||||
export type DocumentResendDialogProps = {
|
||||
document: Document & {
|
||||
team: Pick<Team, 'id' | 'url'> | null;
|
||||
};
|
||||
recipients: Recipient[];
|
||||
team?: Pick<Team, 'id' | 'url'>;
|
||||
};
|
||||
|
||||
export const ZResendDocumentFormSchema = z.object({
|
||||
@ -57,17 +57,15 @@ export const ZResendDocumentFormSchema = z.object({
|
||||
|
||||
export type TResendDocumentFormSchema = z.infer<typeof ZResendDocumentFormSchema>;
|
||||
|
||||
export const ResendDocumentActionItem = ({
|
||||
document,
|
||||
recipients,
|
||||
team,
|
||||
}: ResendDocumentActionItemProps) => {
|
||||
const { data: session } = useSession();
|
||||
export const DocumentResendDialog = ({ document, recipients }: DocumentResendDialogProps) => {
|
||||
const { user } = useSession();
|
||||
const team = useOptionalCurrentTeam();
|
||||
|
||||
const { toast } = useToast();
|
||||
const { _ } = useLingui();
|
||||
|
||||
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 isDisabled =
|
||||
@ -1,10 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { startRegistration } from '@simplewebauthn/browser';
|
||||
import { KeyRoundIcon } from 'lucide-react';
|
||||
@ -38,7 +37,7 @@ import {
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type CreatePasskeyDialogProps = {
|
||||
export type PasskeyCreateDialogProps = {
|
||||
trigger?: React.ReactNode;
|
||||
onSuccess?: () => void;
|
||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||
@ -51,7 +50,7 @@ type TCreatePasskeyFormSchema = z.infer<typeof ZCreatePasskeyFormSchema>;
|
||||
|
||||
const parser = new UAParser();
|
||||
|
||||
export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePasskeyDialogProps) => {
|
||||
export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCreateDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
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();
|
||||
|
||||
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}>
|
||||
{trigger ?? (
|
||||
<Button variant="secondary" loading={isLoading}>
|
||||
<Button variant="secondary" loading={isPending}>
|
||||
<KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />
|
||||
<Trans>Add passkey</Trans>
|
||||
</Button>
|
||||
@ -1,18 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
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 { 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 { CheckCircle2Icon, CircleIcon } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { P, match } from 'ts-pattern';
|
||||
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 {
|
||||
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();
|
||||
|
||||
const setTemplateToPrivate = async (templateId: number) => {
|
||||
@ -1,7 +1,8 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Loader, TagIcon } from 'lucide-react';
|
||||
@ -20,18 +21,18 @@ import {
|
||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type CreateTeamCheckoutDialogProps = {
|
||||
export type TeamCheckoutCreateDialogProps = {
|
||||
pendingTeamId: number | null;
|
||||
onClose: () => void;
|
||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||
|
||||
const MotionCard = motion(Card);
|
||||
|
||||
export const CreateTeamCheckoutDialog = ({
|
||||
export const TeamCheckoutCreateDialog = ({
|
||||
pendingTeamId,
|
||||
onClose,
|
||||
...props
|
||||
}: CreateTeamCheckoutDialogProps) => {
|
||||
}: TeamCheckoutCreateDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
@ -39,7 +40,7 @@ export const CreateTeamCheckoutDialog = ({
|
||||
|
||||
const { data, isLoading } = trpc.team.getTeamPrices.useQuery();
|
||||
|
||||
const { mutateAsync: createCheckout, isLoading: isCreatingCheckout } =
|
||||
const { mutateAsync: createCheckout, isPending: isCreatingCheckout } =
|
||||
trpc.team.createTeamPendingCheckout.useMutation({
|
||||
onSuccess: (checkoutUrl) => {
|
||||
window.open(checkoutUrl, '_blank');
|
||||
@ -1,18 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useSearchParams } from 'react-router';
|
||||
import { useNavigate } from 'react-router';
|
||||
import type { z } from 'zod';
|
||||
|
||||
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 { trpc } from '@documenso/trpc/react';
|
||||
import { ZCreateTeamMutationSchema } from '@documenso/trpc/server/team-router/schema';
|
||||
@ -37,7 +36,7 @@ import {
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type CreateTeamDialogProps = {
|
||||
export type TeamCreateDialogProps = {
|
||||
trigger?: React.ReactNode;
|
||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||
|
||||
@ -48,12 +47,12 @@ const ZCreateTeamFormSchema = ZCreateTeamMutationSchema.pick({
|
||||
|
||||
type TCreateTeamFormSchema = z.infer<typeof ZCreateTeamFormSchema>;
|
||||
|
||||
export const CreateTeamDialog = ({ trigger, ...props }: CreateTeamDialogProps) => {
|
||||
export const TeamCreateDialog = ({ trigger, ...props }: TeamCreateDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
@ -80,7 +79,7 @@ export const CreateTeamDialog = ({ trigger, ...props }: CreateTeamDialogProps) =
|
||||
setOpen(false);
|
||||
|
||||
if (response.paymentRequired) {
|
||||
router.push(`/settings/teams?tab=pending&checkout=${response.pendingTeamId}`);
|
||||
await navigate(`/settings/teams?tab=pending&checkout=${response.pendingTeamId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -201,7 +200,7 @@ export const CreateTeamDialog = ({ trigger, ...props }: CreateTeamDialogProps) =
|
||||
{!form.formState.errors.teamUrl && (
|
||||
<span className="text-foreground/50 text-xs font-normal">
|
||||
{field.value ? (
|
||||
`${WEBAPP_BASE_URL}/t/${field.value}`
|
||||
`${NEXT_PUBLIC_WEBAPP_URL()}/t/${field.value}`
|
||||
) : (
|
||||
<Trans>A unique URL to identify your team</Trans>
|
||||
)}
|
||||
@ -1,13 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
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 { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type DeleteTeamDialogProps = {
|
||||
export type TeamDeleteDialogProps = {
|
||||
teamId: number;
|
||||
teamName: string;
|
||||
trigger?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const DeleteTeamDialog = ({ trigger, teamId, teamName }: DeleteTeamDialogProps) => {
|
||||
const router = useRouter();
|
||||
export const TeamDeleteDialog = ({ trigger, teamId, teamName }: TeamDeleteDialogProps) => {
|
||||
const navigate = useNavigate();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { _ } = useLingui();
|
||||
@ -74,9 +72,9 @@ export const DeleteTeamDialog = ({ trigger, teamId, teamName }: DeleteTeamDialog
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
await navigate('/settings/teams');
|
||||
|
||||
router.push('/settings/teams');
|
||||
setOpen(false);
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
@ -1,15 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRevalidator } from 'react-router';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
@ -36,7 +34,7 @@ import {
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type AddTeamEmailDialogProps = {
|
||||
export type TeamEmailAddDialogProps = {
|
||||
teamId: number;
|
||||
trigger?: React.ReactNode;
|
||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||
@ -48,13 +46,12 @@ const ZCreateTeamEmailFormSchema = ZCreateTeamEmailVerificationMutationSchema.pi
|
||||
|
||||
type TCreateTeamEmailFormSchema = z.infer<typeof ZCreateTeamEmailFormSchema>;
|
||||
|
||||
export const AddTeamEmailDialog = ({ teamId, trigger, ...props }: AddTeamEmailDialogProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
export const TeamEmailAddDialog = ({ teamId, trigger, ...props }: TeamEmailAddDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { revalidate } = useRevalidator();
|
||||
|
||||
const form = useForm<TCreateTeamEmailFormSchema>({
|
||||
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();
|
||||
|
||||
const onFormSubmit = async ({ name, email }: TCreateTeamEmailFormSchema) => {
|
||||
@ -81,7 +78,7 @@ export const AddTeamEmailDialog = ({ teamId, trigger, ...props }: AddTeamEmailDi
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
await revalidate();
|
||||
|
||||
setOpen(false);
|
||||
} catch (err) {
|
||||
@ -120,7 +117,7 @@ export const AddTeamEmailDialog = ({ teamId, trigger, ...props }: AddTeamEmailDi
|
||||
>
|
||||
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
|
||||
{trigger ?? (
|
||||
<Button variant="outline" loading={isLoading} className="bg-background">
|
||||
<Button variant="outline" loading={isPending} className="bg-background">
|
||||
<Plus className="-ml-1 mr-1 h-5 w-5" />
|
||||
<Trans>Add email</Trans>
|
||||
</Button>
|
||||
@ -1,15 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 type { Prisma } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert } from '@documenso/ui/primitives/alert';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
@ -25,7 +23,7 @@ import {
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type RemoveTeamEmailDialogProps = {
|
||||
export type TeamEmailDeleteDialogProps = {
|
||||
trigger?: React.ReactNode;
|
||||
teamName: string;
|
||||
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 { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { revalidate } = useRevalidator();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { mutateAsync: deleteTeamEmail, isLoading: isDeletingTeamEmail } =
|
||||
const { mutateAsync: deleteTeamEmail, isPending: isDeletingTeamEmail } =
|
||||
trpc.team.deleteTeamEmail.useMutation({
|
||||
onSuccess: () => {
|
||||
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({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
@ -97,7 +94,7 @@ export const RemoveTeamEmailDialog = ({ trigger, teamName, team }: RemoveTeamEma
|
||||
await deleteTeamEmailVerification({ teamId: team.id });
|
||||
}
|
||||
|
||||
router.refresh();
|
||||
await revalidate();
|
||||
};
|
||||
|
||||
return (
|
||||
@ -127,7 +124,7 @@ export const RemoveTeamEmailDialog = ({ trigger, teamName, team }: RemoveTeamEma
|
||||
<Alert variant="neutral" padding="tight">
|
||||
<AvatarWithText
|
||||
avatarClass="h-12 w-12"
|
||||
avatarSrc={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${team.avatarImageId}`}
|
||||
avatarSrc={formatAvatarUrl(team.avatarImageId)}
|
||||
avatarFallback={extractInitials(
|
||||
(team.teamEmail?.name || team.emailVerification?.name) ?? '',
|
||||
)}
|
||||
@ -1,17 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 { useForm } from 'react-hook-form';
|
||||
import { useRevalidator } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { TeamEmail } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
@ -34,7 +32,7 @@ import {
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type UpdateTeamEmailDialogProps = {
|
||||
export type TeamEmailUpdateDialogProps = {
|
||||
teamEmail: TeamEmail;
|
||||
trigger?: React.ReactNode;
|
||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||
@ -45,17 +43,16 @@ const ZUpdateTeamEmailFormSchema = z.object({
|
||||
|
||||
type TUpdateTeamEmailFormSchema = z.infer<typeof ZUpdateTeamEmailFormSchema>;
|
||||
|
||||
export const UpdateTeamEmailDialog = ({
|
||||
export const TeamEmailUpdateDialog = ({
|
||||
teamEmail,
|
||||
trigger,
|
||||
...props
|
||||
}: UpdateTeamEmailDialogProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
}: TeamEmailUpdateDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { revalidate } = useRevalidator();
|
||||
|
||||
const form = useForm<TUpdateTeamEmailFormSchema>({
|
||||
resolver: zodResolver(ZUpdateTeamEmailFormSchema),
|
||||
@ -81,7 +78,7 @@ export const UpdateTeamEmailDialog = ({
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
await revalidate();
|
||||
|
||||
setOpen(false);
|
||||
} catch (err) {
|
||||
@ -1,13 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 type { TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert } from '@documenso/ui/primitives/alert';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
@ -23,7 +22,7 @@ import {
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type LeaveTeamDialogProps = {
|
||||
export type TeamLeaveDialogProps = {
|
||||
teamId: number;
|
||||
teamName: string;
|
||||
teamAvatarImageId?: string | null;
|
||||
@ -31,19 +30,19 @@ export type LeaveTeamDialogProps = {
|
||||
trigger?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const LeaveTeamDialog = ({
|
||||
export const TeamLeaveDialog = ({
|
||||
trigger,
|
||||
teamId,
|
||||
teamName,
|
||||
teamAvatarImageId,
|
||||
role,
|
||||
}: LeaveTeamDialogProps) => {
|
||||
}: TeamLeaveDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mutateAsync: leaveTeam, isLoading: isLeavingTeam } = trpc.team.leaveTeam.useMutation({
|
||||
const { mutateAsync: leaveTeam, isPending: isLeavingTeam } = trpc.team.leaveTeam.useMutation({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: _(msg`Success`),
|
||||
@ -89,7 +88,7 @@ export const LeaveTeamDialog = ({
|
||||
<Alert variant="neutral" padding="tight">
|
||||
<AvatarWithText
|
||||
avatarClass="h-12 w-12"
|
||||
avatarSrc={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${teamAvatarImageId}`}
|
||||
avatarSrc={formatAvatarUrl(teamAvatarImageId)}
|
||||
avatarFallback={teamName.slice(0, 1).toUpperCase()}
|
||||
primaryText={teamName}
|
||||
secondaryText={_(TEAM_MEMBER_ROLE_MAP[role])}
|
||||
@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert } from '@documenso/ui/primitives/alert';
|
||||
@ -20,7 +19,7 @@ import {
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type DeleteTeamMemberDialogProps = {
|
||||
export type TeamMemberDeleteDialogProps = {
|
||||
teamId: number;
|
||||
teamName: string;
|
||||
teamMemberId: number;
|
||||
@ -29,20 +28,20 @@ export type DeleteTeamMemberDialogProps = {
|
||||
trigger?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const DeleteTeamMemberDialog = ({
|
||||
export const TeamMemberDeleteDialog = ({
|
||||
trigger,
|
||||
teamId,
|
||||
teamName,
|
||||
teamMemberId,
|
||||
teamMemberName,
|
||||
teamMemberEmail,
|
||||
}: DeleteTeamMemberDialogProps) => {
|
||||
}: TeamMemberDeleteDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mutateAsync: deleteTeamMembers, isLoading: isDeletingTeamMember } =
|
||||
const { mutateAsync: deleteTeamMembers, isPending: isDeletingTeamMember } =
|
||||
trpc.team.deleteTeamMembers.useMutation({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 { Download, Mail, MailIcon, PlusCircle, Trash, Upload, UsersIcon } from 'lucide-react';
|
||||
import Papa, { type ParseResult } from 'papaparse';
|
||||
@ -13,7 +13,6 @@ import { z } from 'zod';
|
||||
|
||||
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
||||
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 { ZCreateTeamMemberInvitesMutationSchema } from '@documenso/trpc/server/team-router/schema';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@ -47,9 +46,9 @@ import {
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type InviteTeamMembersDialogProps = {
|
||||
currentUserTeamRole: TeamMemberRole;
|
||||
teamId: number;
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
export type TeamMemberInviteDialogProps = {
|
||||
trigger?: React.ReactNode;
|
||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||
|
||||
@ -96,12 +95,7 @@ const ZImportTeamMemberSchema = z.array(
|
||||
}),
|
||||
);
|
||||
|
||||
export const InviteTeamMembersDialog = ({
|
||||
currentUserTeamRole,
|
||||
teamId,
|
||||
trigger,
|
||||
...props
|
||||
}: InviteTeamMembersDialogProps) => {
|
||||
export const TeamMemberInviteDialog = ({ trigger, ...props }: TeamMemberInviteDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [invitationType, setInvitationType] = useState<TabTypes>('INDIVIDUAL');
|
||||
@ -109,6 +103,8 @@ export const InviteTeamMembersDialog = ({
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const form = useForm<TInviteTeamMembersFormSchema>({
|
||||
resolver: zodResolver(ZInviteTeamMembersFormSchema),
|
||||
defaultValues: {
|
||||
@ -142,7 +138,7 @@ export const InviteTeamMembersDialog = ({
|
||||
const onFormSubmit = async ({ invitations }: TInviteTeamMembersFormSchema) => {
|
||||
try {
|
||||
await createTeamMemberInvites({
|
||||
teamId,
|
||||
teamId: team.id,
|
||||
invitations,
|
||||
});
|
||||
|
||||
@ -204,7 +200,7 @@ export const InviteTeamMembersDialog = ({
|
||||
|
||||
setInvitationType('INDIVIDUAL');
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
@ -325,11 +321,13 @@ export const InviteTeamMembersDialog = ({
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent position="popper">
|
||||
{TEAM_MEMBER_ROLE_HIERARCHY[currentUserTeamRole].map((role) => (
|
||||
<SelectItem key={role} value={role}>
|
||||
{_(TEAM_MEMBER_ROLE_MAP[role]) ?? role}
|
||||
</SelectItem>
|
||||
))}
|
||||
{TEAM_MEMBER_ROLE_HIERARCHY[team.currentTeamMember.role].map(
|
||||
(role) => (
|
||||
<SelectItem key={role} value={role}>
|
||||
{_(TEAM_MEMBER_ROLE_MAP[role]) ?? role}
|
||||
</SelectItem>
|
||||
),
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
@ -1,17 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
|
||||
import { isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
@ -40,7 +39,7 @@ import {
|
||||
} from '@documenso/ui/primitives/select';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type UpdateTeamMemberDialogProps = {
|
||||
export type TeamMemberUpdateDialogProps = {
|
||||
currentUserTeamRole: TeamMemberRole;
|
||||
trigger?: React.ReactNode;
|
||||
teamId: number;
|
||||
@ -55,7 +54,7 @@ const ZUpdateTeamMemberFormSchema = z.object({
|
||||
|
||||
type ZUpdateTeamMemberSchema = z.infer<typeof ZUpdateTeamMemberFormSchema>;
|
||||
|
||||
export const UpdateTeamMemberDialog = ({
|
||||
export const TeamMemberUpdateDialog = ({
|
||||
currentUserTeamRole,
|
||||
trigger,
|
||||
teamId,
|
||||
@ -63,7 +62,7 @@ export const UpdateTeamMemberDialog = ({
|
||||
teamMemberName,
|
||||
teamMemberRole,
|
||||
...props
|
||||
}: UpdateTeamMemberDialogProps) => {
|
||||
}: TeamMemberUpdateDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { _ } = useLingui();
|
||||
@ -1,14 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRevalidator } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
@ -42,24 +40,24 @@ import {
|
||||
} from '@documenso/ui/primitives/select';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type TransferTeamDialogProps = {
|
||||
export type TeamTransferDialogProps = {
|
||||
teamId: number;
|
||||
teamName: string;
|
||||
ownerUserId: number;
|
||||
trigger?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const TransferTeamDialog = ({
|
||||
export const TeamTransferDialog = ({
|
||||
trigger,
|
||||
teamId,
|
||||
teamName,
|
||||
ownerUserId,
|
||||
}: TransferTeamDialogProps) => {
|
||||
const router = useRouter();
|
||||
}: TeamTransferDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { revalidate } = useRevalidator();
|
||||
|
||||
const { mutateAsync: requestTeamOwnershipTransfer } =
|
||||
trpc.team.requestTeamOwnershipTransfer.useMutation();
|
||||
@ -102,7 +100,7 @@ export const TransferTeamDialog = ({
|
||||
clearPaymentMethods,
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
await revalidate();
|
||||
|
||||
toast({
|
||||
title: _(msg`Success`),
|
||||
@ -1,15 +1,12 @@
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
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 { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -26,21 +23,21 @@ import {
|
||||
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
type NewTemplateDialogProps = {
|
||||
type TemplateCreateDialogProps = {
|
||||
teamId?: number;
|
||||
templateRootPath: string;
|
||||
};
|
||||
|
||||
export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps) => {
|
||||
const router = useRouter();
|
||||
export const TemplateCreateDialog = ({ templateRootPath }: TemplateCreateDialogProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: session } = useSession();
|
||||
const { user } = useSession();
|
||||
const { toast } = useToast();
|
||||
const { _ } = useLingui();
|
||||
|
||||
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
|
||||
|
||||
const [showNewTemplateDialog, setShowNewTemplateDialog] = useState(false);
|
||||
const [showTemplateCreateDialog, setShowTemplateCreateDialog] = useState(false);
|
||||
const [isUploadingFile, setIsUploadingFile] = useState(false);
|
||||
|
||||
const onFileDrop = async (file: File) => {
|
||||
@ -51,15 +48,11 @@ export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps)
|
||||
setIsUploadingFile(true);
|
||||
|
||||
try {
|
||||
const { type, data } = await putPdfFile(file);
|
||||
const { id: templateDocumentDataId } = await createDocumentData({
|
||||
type,
|
||||
data,
|
||||
});
|
||||
const response = await putPdfFile(file);
|
||||
|
||||
const { id } = await createTemplate({
|
||||
title: file.name,
|
||||
templateDocumentDataId,
|
||||
templateDocumentDataId: response.id,
|
||||
});
|
||||
|
||||
toast({
|
||||
@ -70,9 +63,9 @@ export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps)
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
setShowNewTemplateDialog(false);
|
||||
setShowTemplateCreateDialog(false);
|
||||
|
||||
router.push(`${templateRootPath}/${id}/edit`);
|
||||
await navigate(`${templateRootPath}/${id}/edit`);
|
||||
} catch {
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
@ -86,11 +79,12 @@ export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps)
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={showNewTemplateDialog}
|
||||
onOpenChange={(value) => !isUploadingFile && setShowNewTemplateDialog(value)}
|
||||
open={showTemplateCreateDialog}
|
||||
onOpenChange={(value) => !isUploadingFile && setShowTemplateCreateDialog(value)}
|
||||
>
|
||||
<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" />
|
||||
<Trans>New Template</Trans>
|
||||
</Button>
|
||||
@ -1,7 +1,6 @@
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -15,22 +14,25 @@ import {
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
type DeleteTemplateDialogProps = {
|
||||
type TemplateDeleteDialogProps = {
|
||||
id: number;
|
||||
teamId?: number;
|
||||
open: boolean;
|
||||
onOpenChange: (_open: boolean) => void;
|
||||
onDelete?: () => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateDialogProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
export const TemplateDeleteDialog = ({
|
||||
id,
|
||||
open,
|
||||
onOpenChange,
|
||||
onDelete,
|
||||
}: TemplateDeleteDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mutateAsync: deleteTemplate, isLoading } = trpcReact.template.deleteTemplate.useMutation({
|
||||
onSuccess: () => {
|
||||
router.refresh();
|
||||
const { mutateAsync: deleteTemplate, isPending } = trpcReact.template.deleteTemplate.useMutation({
|
||||
onSuccess: async () => {
|
||||
await onDelete?.();
|
||||
|
||||
toast({
|
||||
title: _(msg`Template deleted`),
|
||||
@ -51,7 +53,7 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
|
||||
<Dialog open={open} onOpenChange={(value) => !isPending && onOpenChange(value)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
@ -70,7 +72,7 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={isLoading}
|
||||
disabled={isPending}
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
@ -79,7 +81,7 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
loading={isLoading}
|
||||
loading={isPending}
|
||||
onClick={async () => deleteTemplate({ templateId: id })}
|
||||
>
|
||||
<Trans>Delete</Trans>
|
||||
@ -1,17 +1,15 @@
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { Recipient, Template, TemplateDirectLink } from '@prisma/client';
|
||||
import { LinkIcon } from 'lucide-react';
|
||||
|
||||
import type { Recipient, Template, TemplateDirectLink } from '@documenso/prisma/client';
|
||||
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 = {
|
||||
template: Template & { directLink?: TemplateDirectLink | null; Recipient: Recipient[] };
|
||||
template: Template & { directLink?: TemplateDirectLink | null; recipients: Recipient[] };
|
||||
};
|
||||
|
||||
export const TemplateDirectLinkDialogWrapper = ({
|
||||
@ -1,11 +1,16 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 { Link, useRevalidator } from 'react-router';
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
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 { DIRECT_TEMPLATE_DOCUMENTATION } from '@documenso/lib/constants/template';
|
||||
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 { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
@ -46,12 +45,10 @@ import {
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
type TemplateDirectLinkDialogProps = {
|
||||
template: Template & {
|
||||
directLink?: Pick<TemplateDirectLink, 'token' | 'enabled'> | null;
|
||||
Recipient: Recipient[];
|
||||
recipients: Recipient[];
|
||||
};
|
||||
open: boolean;
|
||||
onOpenChange: (_open: boolean) => void;
|
||||
@ -67,11 +64,9 @@ export const TemplateDirectLinkDialog = ({
|
||||
const { toast } = useToast();
|
||||
const { quota, remaining } = useLimits();
|
||||
const { _ } = useLingui();
|
||||
|
||||
const team = useOptionalCurrentTeam();
|
||||
const { revalidate } = useRevalidator();
|
||||
|
||||
const [, copy] = useCopyToClipboard();
|
||||
const router = useRouter();
|
||||
|
||||
const [isEnabled, setIsEnabled] = useState(template.directLink?.enabled ?? false);
|
||||
const [token, setToken] = useState(template.directLink?.token ?? null);
|
||||
@ -81,21 +76,21 @@ export const TemplateDirectLinkDialog = ({
|
||||
);
|
||||
|
||||
const validDirectTemplateRecipients = useMemo(
|
||||
() => template.Recipient.filter((recipient) => recipient.role !== RecipientRole.CC),
|
||||
[template.Recipient],
|
||||
() => template.recipients.filter((recipient) => recipient.role !== RecipientRole.CC),
|
||||
[template.recipients],
|
||||
);
|
||||
|
||||
const {
|
||||
mutateAsync: createTemplateDirectLink,
|
||||
isLoading: isCreatingTemplateDirectLink,
|
||||
isPending: isCreatingTemplateDirectLink,
|
||||
reset: resetCreateTemplateDirectLink,
|
||||
} = trpcReact.template.createTemplateDirectLink.useMutation({
|
||||
onSuccess: (data) => {
|
||||
onSuccess: async (data) => {
|
||||
await revalidate();
|
||||
|
||||
setToken(data.token);
|
||||
setIsEnabled(data.enabled);
|
||||
setCurrentStep('MANAGE');
|
||||
|
||||
router.refresh();
|
||||
},
|
||||
onError: () => {
|
||||
setSelectedRecipientId(null);
|
||||
@ -108,9 +103,11 @@ export const TemplateDirectLinkDialog = ({
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: toggleTemplateDirectLink, isLoading: isTogglingTemplateAccess } =
|
||||
const { mutateAsync: toggleTemplateDirectLink, isPending: isTogglingTemplateAccess } =
|
||||
trpcReact.template.toggleTemplateDirectLink.useMutation({
|
||||
onSuccess: (data) => {
|
||||
onSuccess: async (data) => {
|
||||
await revalidate();
|
||||
|
||||
const enabledDescription = msg`Direct link signing has been enabled`;
|
||||
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({
|
||||
onSuccess: () => {
|
||||
onSuccess: async () => {
|
||||
await revalidate();
|
||||
|
||||
onOpenChange(false);
|
||||
setToken(null);
|
||||
|
||||
@ -143,7 +142,6 @@ export const TemplateDirectLinkDialog = ({
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
setToken(null);
|
||||
},
|
||||
onError: () => {
|
||||
@ -235,7 +233,7 @@ export const TemplateDirectLinkDialog = ({
|
||||
templates.{' '}
|
||||
<Link
|
||||
className="mt-1 block underline underline-offset-4"
|
||||
href="/settings/billing"
|
||||
to="/settings/billing"
|
||||
>
|
||||
Upgrade your account to continue!
|
||||
</Link>
|
||||
@ -326,7 +324,7 @@ export const TemplateDirectLinkDialog = ({
|
||||
</div>
|
||||
|
||||
{/* Prevent creating placeholder direct template recipient if the email already exists. */}
|
||||
{!template.Recipient.some(
|
||||
{!template.recipients.some(
|
||||
(recipient) => recipient.email === DIRECT_TEMPLATE_RECIPIENT_EMAIL,
|
||||
) && (
|
||||
<DialogFooter className="mx-auto">
|
||||
@ -436,7 +434,7 @@ export const TemplateDirectLinkDialog = ({
|
||||
await toggleTemplateDirectLink({
|
||||
templateId: template.id,
|
||||
enabled: isEnabled,
|
||||
}).catch((e) => null);
|
||||
}).catch(() => null);
|
||||
|
||||
onOpenChange(false);
|
||||
}}
|
||||
@ -1,7 +1,6 @@
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -15,28 +14,23 @@ import {
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
type DuplicateTemplateDialogProps = {
|
||||
type TemplateDuplicateDialogProps = {
|
||||
id: number;
|
||||
teamId?: number;
|
||||
open: boolean;
|
||||
onOpenChange: (_open: boolean) => void;
|
||||
};
|
||||
|
||||
export const DuplicateTemplateDialog = ({
|
||||
export const TemplateDuplicateDialog = ({
|
||||
id,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: DuplicateTemplateDialogProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
}: TemplateDuplicateDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mutateAsync: duplicateTemplate, isLoading } =
|
||||
const { mutateAsync: duplicateTemplate, isPending } =
|
||||
trpcReact.template.duplicateTemplate.useMutation({
|
||||
onSuccess: () => {
|
||||
router.refresh();
|
||||
|
||||
toast({
|
||||
title: _(msg`Template duplicated`),
|
||||
description: _(msg`Your template has been duplicated successfully.`),
|
||||
@ -55,7 +49,7 @@ export const DuplicateTemplateDialog = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
|
||||
<Dialog open={open} onOpenChange={(value) => !isPending && onOpenChange(value)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
@ -70,7 +64,7 @@ export const DuplicateTemplateDialog = ({
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={isLoading}
|
||||
disabled={isPending}
|
||||
variant="secondary"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
@ -79,7 +73,7 @@ export const DuplicateTemplateDialog = ({
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
loading={isLoading}
|
||||
loading={isPending}
|
||||
onClick={async () =>
|
||||
duplicateTemplate({
|
||||
templateId: id,
|
||||
@ -1,13 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
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 { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -28,29 +27,46 @@ import {
|
||||
} from '@documenso/ui/primitives/select';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
type MoveTemplateDialogProps = {
|
||||
type TemplateMoveDialogProps = {
|
||||
templateId: number;
|
||||
open: boolean;
|
||||
onOpenChange: (_open: boolean) => void;
|
||||
onMove?: ({
|
||||
templateId,
|
||||
teamUrl,
|
||||
}: {
|
||||
templateId: number;
|
||||
teamUrl: string;
|
||||
}) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTemplateDialogProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
export const TemplateMoveDialog = ({
|
||||
templateId,
|
||||
open,
|
||||
onOpenChange,
|
||||
onMove,
|
||||
}: TemplateMoveDialogProps) => {
|
||||
const { toast } = useToast();
|
||||
const { _ } = useLingui();
|
||||
|
||||
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
|
||||
|
||||
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
|
||||
const { mutateAsync: moveTemplate, isLoading } = trpc.template.moveTemplateToTeam.useMutation({
|
||||
onSuccess: () => {
|
||||
router.refresh();
|
||||
|
||||
const { mutateAsync: moveTemplate, isPending } = trpc.template.moveTemplateToTeam.useMutation({
|
||||
onSuccess: async () => {
|
||||
const team = teams?.find((team) => team.id === selectedTeamId);
|
||||
|
||||
if (team) {
|
||||
await onMove?.({ templateId, teamUrl: team.url });
|
||||
}
|
||||
|
||||
toast({
|
||||
title: _(msg`Template moved`),
|
||||
description: _(msg`The template has been successfully moved to the selected team.`),
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
onOpenChange(false);
|
||||
},
|
||||
onError: (err) => {
|
||||
@ -73,7 +89,7 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
|
||||
},
|
||||
});
|
||||
|
||||
const onMove = async () => {
|
||||
const handleOnMove = async () => {
|
||||
if (!selectedTeamId) {
|
||||
return;
|
||||
}
|
||||
@ -108,9 +124,7 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="h-8 w-8">
|
||||
{team.avatarImageId && (
|
||||
<AvatarImage
|
||||
src={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${team.avatarImageId}`}
|
||||
/>
|
||||
<AvatarImage src={formatAvatarUrl(team.avatarImageId)} />
|
||||
)}
|
||||
|
||||
<AvatarFallback className="text-sm text-gray-400">
|
||||
@ -130,8 +144,12 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
|
||||
<Button variant="secondary" onClick={() => onOpenChange(false)}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
<Button onClick={onMove} loading={isLoading} disabled={!selectedTeamId || isLoading}>
|
||||
{isLoading ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
|
||||
<Button
|
||||
onClick={handleOnMove}
|
||||
loading={isPending}
|
||||
disabled={!selectedTeamId || isPending}
|
||||
>
|
||||
{isPending ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@ -1,14 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||
@ -18,8 +18,6 @@ import {
|
||||
} from '@documenso/lib/constants/template';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
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 { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -94,7 +92,7 @@ const ZAddRecipientsForNewDocumentSchema = z
|
||||
|
||||
type TAddRecipientsForNewDocumentSchema = z.infer<typeof ZAddRecipientsForNewDocumentSchema>;
|
||||
|
||||
export type UseTemplateDialogProps = {
|
||||
export type TemplateUseDialogProps = {
|
||||
templateId: number;
|
||||
templateSigningOrder?: DocumentSigningOrder | null;
|
||||
recipients: Recipient[];
|
||||
@ -103,19 +101,19 @@ export type UseTemplateDialogProps = {
|
||||
trigger?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function UseTemplateDialog({
|
||||
export function TemplateUseDialog({
|
||||
recipients,
|
||||
documentDistributionMethod = DocumentDistributionMethod.EMAIL,
|
||||
documentRootPath,
|
||||
templateId,
|
||||
templateSigningOrder,
|
||||
trigger,
|
||||
}: UseTemplateDialogProps) {
|
||||
const router = useRouter();
|
||||
|
||||
}: TemplateUseDialogProps) {
|
||||
const { toast } = useToast();
|
||||
const { _ } = useLingui();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const form = useForm<TAddRecipientsForNewDocumentSchema>({
|
||||
@ -179,7 +177,7 @@ export function UseTemplateDialog({
|
||||
documentPath += '?action=view-signing-links';
|
||||
}
|
||||
|
||||
router.push(documentPath);
|
||||
await navigate(documentPath);
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
@ -1,16 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { ApiToken } from '@prisma/client';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { ApiToken } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
@ -33,35 +30,33 @@ import {
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type DeleteTokenDialogProps = {
|
||||
export type TokenDeleteDialogProps = {
|
||||
teamId?: number;
|
||||
token: Pick<ApiToken, 'id' | 'name'>;
|
||||
onDelete?: () => void;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function DeleteTokenDialog({
|
||||
export default function TokenDeleteDialog({
|
||||
teamId,
|
||||
token,
|
||||
onDelete,
|
||||
children,
|
||||
}: DeleteTokenDialogProps) {
|
||||
}: TokenDeleteDialogProps) {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const deleteMessage = _(msg`delete ${token.name}`);
|
||||
|
||||
const ZDeleteTokenDialogSchema = z.object({
|
||||
const ZTokenDeleteDialogSchema = z.object({
|
||||
tokenName: z.literal(deleteMessage, {
|
||||
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({
|
||||
onSuccess() {
|
||||
@ -70,7 +65,7 @@ export default function DeleteTokenDialog({
|
||||
});
|
||||
|
||||
const form = useForm<TDeleteTokenByIdMutationSchema>({
|
||||
resolver: zodResolver(ZDeleteTokenDialogSchema),
|
||||
resolver: zodResolver(ZTokenDeleteDialogSchema),
|
||||
values: {
|
||||
tokenName: '',
|
||||
},
|
||||
@ -90,8 +85,6 @@ export default function DeleteTokenDialog({
|
||||
});
|
||||
|
||||
setIsOpen(false);
|
||||
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
@ -1,12 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import type { z } from 'zod';
|
||||
@ -39,22 +36,20 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
import { TriggerMultiSelectCombobox } from './trigger-multiselect-combobox';
|
||||
import { WebhookMultiSelectCombobox } from '../general/webhook-multiselect-combobox';
|
||||
|
||||
const ZCreateWebhookFormSchema = ZCreateWebhookMutationSchema.omit({ teamId: true });
|
||||
|
||||
type TCreateWebhookFormSchema = z.infer<typeof ZCreateWebhookFormSchema>;
|
||||
|
||||
export type CreateWebhookDialogProps = {
|
||||
export type WebhookCreateDialogProps = {
|
||||
trigger?: React.ReactNode;
|
||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||
|
||||
export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogProps) => {
|
||||
export const WebhookCreateDialog = ({ trigger, ...props }: WebhookCreateDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const team = useOptionalCurrentTeam();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
@ -94,8 +89,6 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr
|
||||
});
|
||||
|
||||
form.reset();
|
||||
|
||||
router.refresh();
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
@ -191,7 +184,7 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr
|
||||
<Trans>Triggers</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<TriggerMultiSelectCombobox
|
||||
<WebhookMultiSelectCombobox
|
||||
listValues={value}
|
||||
onChange={(values: string[]) => {
|
||||
onChange(values);
|
||||
@ -1,16 +1,13 @@
|
||||
'use effect';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { Webhook } from '@prisma/client';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { Webhook } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
@ -35,18 +32,16 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
export type DeleteWebhookDialogProps = {
|
||||
export type WebhookDeleteDialogProps = {
|
||||
webhook: Pick<Webhook, 'id' | 'webhookUrl'>;
|
||||
onDelete?: () => void;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogProps) => {
|
||||
export const WebhookDeleteDialog = ({ webhook, children }: WebhookDeleteDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const team = useOptionalCurrentTeam();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
@ -81,8 +76,6 @@ export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogPr
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
toast({
|
||||
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 { Logo } from '~/components/branding/logo';
|
||||
import { SignInForm } from '~/components/forms/signin';
|
||||
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||
|
||||
export type EmbedAuthenticateViewProps = {
|
||||
export type EmbedAuthenticationRequiredProps = {
|
||||
email?: string;
|
||||
returnTo: string;
|
||||
};
|
||||
|
||||
export const EmbedAuthenticateView = ({ email, returnTo }: EmbedAuthenticateViewProps) => {
|
||||
export const EmbedAuthenticationRequired = ({
|
||||
email,
|
||||
returnTo,
|
||||
}: EmbedAuthenticationRequiredProps) => {
|
||||
return (
|
||||
<div className="flex min-h-[100dvh] w-full items-center justify-center">
|
||||
<div className="flex w-full max-w-md flex-col">
|
||||
<Logo className="h-8" />
|
||||
<BrandingLogo className="h-8" />
|
||||
|
||||
<Alert className="mt-8" variant="warning">
|
||||
<AlertDescription>
|
||||
@ -1,21 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 { DateTime } from 'luxon';
|
||||
import { useSearchParams } from 'react-router';
|
||||
|
||||
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||
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 type {
|
||||
TRemovedSignedFieldWithTokenMutationSchema,
|
||||
@ -31,15 +29,15 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import type { DirectTemplateLocalField } from '~/app/(recipient)/d/[token]/sign-direct-template';
|
||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
||||
import { Logo } from '~/components/branding/logo';
|
||||
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||
import { ZDirectTemplateEmbedDataSchema } from '~/types/embed-direct-template-schema';
|
||||
import { injectCss } from '~/utils/css-vars';
|
||||
|
||||
import { EmbedClientLoading } from '../../client-loading';
|
||||
import { EmbedDocumentCompleted } from '../../completed';
|
||||
import { EmbedDocumentFields } from '../../document-fields';
|
||||
import { injectCss } from '../../util';
|
||||
import { ZDirectTemplateEmbedDataSchema } from './schema';
|
||||
import type { DirectTemplateLocalField } from '../general/direct-template/direct-template-signing-form';
|
||||
import { useRequiredDocumentSigningContext } from '../general/document-signing/document-signing-provider';
|
||||
import { EmbedClientLoading } from './embed-client-loading';
|
||||
import { EmbedDocumentCompleted } from './embed-document-completed';
|
||||
import { EmbedDocumentFields } from './embed-document-fields';
|
||||
|
||||
export type EmbedDirectTemplateClientPageProps = {
|
||||
token: string;
|
||||
@ -65,7 +63,7 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const {
|
||||
fullName,
|
||||
@ -76,7 +74,7 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
setEmail,
|
||||
setSignature,
|
||||
setSignatureValid,
|
||||
} = useRequiredSigningContext();
|
||||
} = useRequiredDocumentSigningContext();
|
||||
|
||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
||||
@ -100,7 +98,7 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
|
||||
const hasSignatureField = localFields.some((field) => field.type === FieldType.SIGNATURE);
|
||||
|
||||
const { mutateAsync: createDocumentFromDirectTemplate, isLoading: isSubmitting } =
|
||||
const { mutateAsync: createDocumentFromDirectTemplate, isPending: isSubmitting } =
|
||||
trpc.template.createDocumentFromDirectTemplate.useMutation();
|
||||
|
||||
const onSignField = (payload: TSignFieldWithTokenMutationSchema) => {
|
||||
@ -118,7 +116,7 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
});
|
||||
|
||||
if (field.type === FieldType.SIGNATURE) {
|
||||
newField.Signature = {
|
||||
newField.signature = {
|
||||
id: 1,
|
||||
created: new Date(),
|
||||
recipientId: 1,
|
||||
@ -163,7 +161,7 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
customText: '',
|
||||
inserted: false,
|
||||
signedValue: undefined,
|
||||
Signature: undefined,
|
||||
signature: undefined,
|
||||
});
|
||||
}),
|
||||
);
|
||||
@ -496,7 +494,7 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
{!hidePoweredBy && (
|
||||
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
|
||||
<span>Powered by</span>
|
||||
<Logo className="ml-2 inline-block h-[14px]" />
|
||||
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
||||
</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 type { Signature } from '@documenso/prisma/client';
|
||||
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
||||
|
||||
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 { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
@ -12,8 +12,6 @@ import {
|
||||
ZRadioFieldMeta,
|
||||
ZTextFieldMeta,
|
||||
} 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 {
|
||||
TRemovedSignedFieldWithTokenMutationSchema,
|
||||
@ -21,16 +19,16 @@ import type {
|
||||
} from '@documenso/trpc/server/field-router/schema';
|
||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||
|
||||
import { CheckboxField } from '~/app/(signing)/sign/[token]/checkbox-field';
|
||||
import { DateField } from '~/app/(signing)/sign/[token]/date-field';
|
||||
import { DropdownField } from '~/app/(signing)/sign/[token]/dropdown-field';
|
||||
import { EmailField } from '~/app/(signing)/sign/[token]/email-field';
|
||||
import { InitialsField } from '~/app/(signing)/sign/[token]/initials-field';
|
||||
import { NameField } from '~/app/(signing)/sign/[token]/name-field';
|
||||
import { NumberField } from '~/app/(signing)/sign/[token]/number-field';
|
||||
import { RadioField } from '~/app/(signing)/sign/[token]/radio-field';
|
||||
import { SignatureField } from '~/app/(signing)/sign/[token]/signature-field';
|
||||
import { TextField } from '~/app/(signing)/sign/[token]/text-field';
|
||||
import { DocumentSigningCheckboxField } from '~/components/general/document-signing/document-signing-checkbox-field';
|
||||
import { DocumentSigningDateField } from '~/components/general/document-signing/document-signing-date-field';
|
||||
import { DocumentSigningDropdownField } from '~/components/general/document-signing/document-signing-dropdown-field';
|
||||
import { DocumentSigningEmailField } from '~/components/general/document-signing/document-signing-email-field';
|
||||
import { DocumentSigningInitialsField } from '~/components/general/document-signing/document-signing-initials-field';
|
||||
import { DocumentSigningNameField } from '~/components/general/document-signing/document-signing-name-field';
|
||||
import { DocumentSigningNumberField } from '~/components/general/document-signing/document-signing-number-field';
|
||||
import { DocumentSigningRadioField } from '~/components/general/document-signing/document-signing-radio-field';
|
||||
import { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-field';
|
||||
import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
|
||||
|
||||
export type EmbedDocumentFieldsProps = {
|
||||
recipient: Recipient;
|
||||
@ -52,7 +50,7 @@ export const EmbedDocumentFields = ({
|
||||
{fields.map((field) =>
|
||||
match(field.type)
|
||||
.with(FieldType.SIGNATURE, () => (
|
||||
<SignatureField
|
||||
<DocumentSigningSignatureField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={recipient}
|
||||
@ -62,7 +60,7 @@ export const EmbedDocumentFields = ({
|
||||
/>
|
||||
))
|
||||
.with(FieldType.INITIALS, () => (
|
||||
<InitialsField
|
||||
<DocumentSigningInitialsField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={recipient}
|
||||
@ -71,7 +69,7 @@ export const EmbedDocumentFields = ({
|
||||
/>
|
||||
))
|
||||
.with(FieldType.NAME, () => (
|
||||
<NameField
|
||||
<DocumentSigningNameField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={recipient}
|
||||
@ -80,7 +78,7 @@ export const EmbedDocumentFields = ({
|
||||
/>
|
||||
))
|
||||
.with(FieldType.DATE, () => (
|
||||
<DateField
|
||||
<DocumentSigningDateField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={recipient}
|
||||
@ -91,7 +89,7 @@ export const EmbedDocumentFields = ({
|
||||
/>
|
||||
))
|
||||
.with(FieldType.EMAIL, () => (
|
||||
<EmailField
|
||||
<DocumentSigningEmailField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={recipient}
|
||||
@ -106,7 +104,7 @@ export const EmbedDocumentFields = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<TextField
|
||||
<DocumentSigningTextField
|
||||
key={field.id}
|
||||
field={fieldWithMeta}
|
||||
recipient={recipient}
|
||||
@ -122,7 +120,7 @@ export const EmbedDocumentFields = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<NumberField
|
||||
<DocumentSigningNumberField
|
||||
key={field.id}
|
||||
field={fieldWithMeta}
|
||||
recipient={recipient}
|
||||
@ -138,7 +136,7 @@ export const EmbedDocumentFields = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<RadioField
|
||||
<DocumentSigningRadioField
|
||||
key={field.id}
|
||||
field={fieldWithMeta}
|
||||
recipient={recipient}
|
||||
@ -154,7 +152,7 @@ export const EmbedDocumentFields = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<CheckboxField
|
||||
<DocumentSigningCheckboxField
|
||||
key={field.id}
|
||||
field={fieldWithMeta}
|
||||
recipient={recipient}
|
||||
@ -170,7 +168,7 @@ export const EmbedDocumentFields = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownField
|
||||
<DocumentSigningDropdownField
|
||||
key={field.id}
|
||||
field={fieldWithMeta}
|
||||
recipient={recipient}
|
||||
@ -1,16 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { DocumentMeta, Recipient, TemplateMeta } from '@prisma/client';
|
||||
import { type DocumentData, type Field, FieldType } from '@prisma/client';
|
||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||
|
||||
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||
import 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 { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||
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 { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
||||
import { Logo } from '~/components/branding/logo';
|
||||
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||
import { injectCss } from '~/utils/css-vars';
|
||||
|
||||
import { EmbedClientLoading } from '../../client-loading';
|
||||
import { EmbedDocumentCompleted } from '../../completed';
|
||||
import { EmbedDocumentFields } from '../../document-fields';
|
||||
import { injectCss } from '../../util';
|
||||
import { ZSignDocumentEmbedDataSchema } from './schema';
|
||||
import { ZSignDocumentEmbedDataSchema } from '../../types/embed-document-sign-schema';
|
||||
import { useRequiredDocumentSigningContext } from '../general/document-signing/document-signing-provider';
|
||||
import { EmbedClientLoading } from './embed-client-loading';
|
||||
import { EmbedDocumentCompleted } from './embed-document-completed';
|
||||
import { EmbedDocumentFields } from './embed-document-fields';
|
||||
|
||||
export type EmbedSignDocumentClientPageProps = {
|
||||
token: string;
|
||||
@ -65,7 +64,7 @@ export const EmbedSignDocumentClientPage = ({
|
||||
setFullName,
|
||||
setSignature,
|
||||
setSignatureValid,
|
||||
} = useRequiredSigningContext();
|
||||
} = useRequiredDocumentSigningContext();
|
||||
|
||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
||||
@ -84,7 +83,7 @@ export const EmbedSignDocumentClientPage = ({
|
||||
fields.filter((field) => field.inserted),
|
||||
];
|
||||
|
||||
const { mutateAsync: completeDocumentWithToken, isLoading: isSubmitting } =
|
||||
const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } =
|
||||
trpc.recipient.completeDocumentWithToken.useMutation();
|
||||
|
||||
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
|
||||
@ -369,7 +368,7 @@ export const EmbedSignDocumentClientPage = ({
|
||||
{!hidePoweredBy && (
|
||||
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
|
||||
<span>Powered by</span>
|
||||
<Logo className="ml-2 inline-block h-[14px]" />
|
||||
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -1,14 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRevalidator } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
@ -42,10 +40,9 @@ export const ZDisable2FAForm = z.object({
|
||||
export type TDisable2FAForm = z.infer<typeof ZDisable2FAForm>;
|
||||
|
||||
export const DisableAuthenticatorAppDialog = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { revalidate } = useRevalidator();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [twoFactorDisableMethod, setTwoFactorDisableMethod] = useState<'totp' | 'backup'>('totp');
|
||||
@ -97,7 +94,7 @@ export const DisableAuthenticatorAppDialog = () => {
|
||||
onCloseTwoFactorDisableDialog();
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
await revalidate();
|
||||
} catch (_err) {
|
||||
toast({
|
||||
title: _(msg`Unable to disable two-factor authentication`),
|
||||
@ -1,13 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRevalidator } from 'react-router';
|
||||
import { renderSVG } from 'uqr';
|
||||
import { z } from 'zod';
|
||||
|
||||
@ -50,8 +48,7 @@ export type EnableAuthenticatorAppDialogProps = {
|
||||
export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorAppDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const router = useRouter();
|
||||
const { revalidate } = useRevalidator();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [recoveryCodes, setRecoveryCodes] = useState<string[] | null>(null);
|
||||
@ -61,7 +58,7 @@ export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorA
|
||||
const {
|
||||
mutateAsync: setup2FA,
|
||||
data: setup2FAData,
|
||||
isLoading: isSettingUp2FA,
|
||||
isPending: isSettingUp2FA,
|
||||
} = trpc.twoFactorAuthentication.setup.useMutation({
|
||||
onError: () => {
|
||||
toast({
|
||||
@ -133,7 +130,7 @@ export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorA
|
||||
|
||||
if (!isOpen && recoveryCodes && recoveryCodes.length > 0) {
|
||||
setRecoveryCodes(null);
|
||||
router.refresh();
|
||||
void revalidate();
|
||||
}
|
||||
|
||||
// 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 { Copy } from 'lucide-react';
|
||||
|
||||
@ -1,16 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -47,7 +44,7 @@ export const ViewRecoveryCodesDialog = () => {
|
||||
const {
|
||||
data: recoveryCodes,
|
||||
mutate,
|
||||
isLoading,
|
||||
isPending,
|
||||
error,
|
||||
} = trpc.twoFactorAuthentication.viewRecoveryCodes.useMutation();
|
||||
|
||||
@ -121,7 +118,7 @@ export const ViewRecoveryCodesDialog = () => {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<fieldset className="flex flex-col space-y-4" disabled={isLoading}>
|
||||
<fieldset className="flex flex-col space-y-4" disabled={isPending}>
|
||||
<FormField
|
||||
name="token"
|
||||
control={viewRecoveryCodesForm.control}
|
||||
@ -147,7 +144,7 @@ export const ViewRecoveryCodesDialog = () => {
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
{match(AppError.parseError(error).message)
|
||||
.with(ErrorCode.INCORRECT_TWO_FACTOR_CODE, () => (
|
||||
.with('INCORRECT_TWO_FACTOR_CODE', () => (
|
||||
<Trans>Invalid code. Please try again.</Trans>
|
||||
))
|
||||
.otherwise(() => (
|
||||
@ -164,7 +161,7 @@ export const ViewRecoveryCodesDialog = () => {
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
<Button type="submit" loading={isLoading}>
|
||||
<Button type="submit" loading={isPending}>
|
||||
<Trans>View</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@ -1,22 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { ErrorCode, useDropzone } from 'react-dropzone';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRevalidator } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
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 { base64 } from '@documenso/lib/universal/base64';
|
||||
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
import type { Team, User } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||
@ -31,6 +29,8 @@ import {
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
export const ZAvatarImageFormSchema = z.object({
|
||||
bytes: z.string().nullish(),
|
||||
});
|
||||
@ -39,15 +39,15 @@ export type TAvatarImageFormSchema = z.infer<typeof ZAvatarImageFormSchema>;
|
||||
|
||||
export type AvatarImageFormProps = {
|
||||
className?: string;
|
||||
user: User;
|
||||
team?: Team;
|
||||
};
|
||||
|
||||
export const AvatarImageForm = ({ className, user, team }: AvatarImageFormProps) => {
|
||||
export const AvatarImageForm = ({ className }: AvatarImageFormProps) => {
|
||||
const { user } = useSession();
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { revalidate } = useRevalidator();
|
||||
|
||||
const router = useRouter();
|
||||
const team = useOptionalCurrentTeam();
|
||||
|
||||
const { mutateAsync: setProfileImage } = trpc.profile.setProfileImage.useMutation();
|
||||
|
||||
@ -109,7 +109,7 @@ export const AvatarImageForm = ({ className, user, team }: AvatarImageFormProps)
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
void revalidate();
|
||||
} catch (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="relative">
|
||||
<Avatar className="h-16 w-16 border-2 border-solid">
|
||||
{avatarImageId && (
|
||||
<AvatarImage
|
||||
src={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${avatarImageId}`}
|
||||
/>
|
||||
)}
|
||||
{avatarImageId && <AvatarImage src={formatAvatarUrl(avatarImageId)} />}
|
||||
<AvatarFallback className="text-sm text-gray-400">
|
||||
{initials}
|
||||
</AvatarFallback>
|
||||
@ -1,14 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { authClient } from '@documenso/auth/client';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
@ -36,7 +34,7 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const form = useForm<TForgotPasswordFormSchema>({
|
||||
values: {
|
||||
@ -47,10 +45,10 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
|
||||
|
||||
const isSubmitting = form.formState.isSubmitting;
|
||||
|
||||
const { mutateAsync: forgotPassword } = trpc.profile.forgotPassword.useMutation();
|
||||
|
||||
const onFormSubmit = async ({ email }: TForgotPasswordFormSchema) => {
|
||||
await forgotPassword({ email }).catch(() => null);
|
||||
await authClient.emailPassword.forgotPassword({ email }).catch(() => null);
|
||||
|
||||
await navigate('/check-email');
|
||||
|
||||
toast({
|
||||
title: _(msg`Reset email sent`),
|
||||
@ -61,8 +59,6 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
|
||||
});
|
||||
|
||||
form.reset();
|
||||
|
||||
router.push('/check-email');
|
||||
};
|
||||
|
||||
return (
|
||||
@ -1,15 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { match } from 'ts-pattern';
|
||||
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 type { User } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { ZCurrentPasswordSchema, ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -39,7 +38,7 @@ export type TPasswordFormSchema = z.infer<typeof ZPasswordFormSchema>;
|
||||
|
||||
export type PasswordFormProps = {
|
||||
className?: string;
|
||||
user: User;
|
||||
user: SessionUser;
|
||||
};
|
||||
|
||||
export const PasswordForm = ({ className }: PasswordFormProps) => {
|
||||
@ -57,11 +56,9 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
|
||||
|
||||
const isSubmitting = form.formState.isSubmitting;
|
||||
|
||||
const { mutateAsync: updatePassword } = trpc.profile.updatePassword.useMutation();
|
||||
|
||||
const onFormSubmit = async ({ currentPassword, password }: TPasswordFormSchema) => {
|
||||
try {
|
||||
await updatePassword({
|
||||
await authClient.emailPassword.updatePassword({
|
||||
currentPassword,
|
||||
password,
|
||||
});
|
||||
@ -1,15 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRevalidator } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import { TRPCClientError } from '@documenso/trpc/client';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -40,14 +37,13 @@ export type TProfileFormSchema = z.infer<typeof ZProfileFormSchema>;
|
||||
|
||||
export type ProfileFormProps = {
|
||||
className?: string;
|
||||
user: User;
|
||||
};
|
||||
|
||||
export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
export const ProfileForm = ({ className }: ProfileFormProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { user } = useSession();
|
||||
const { revalidate } = useRevalidator();
|
||||
|
||||
const form = useForm<TProfileFormSchema>({
|
||||
values: {
|
||||
@ -74,23 +70,15 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
await revalidate();
|
||||
} catch (err) {
|
||||
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
||||
toast({
|
||||
title: _(msg`An error occurred`),
|
||||
description: err.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
description: _(
|
||||
msg`We encountered an unknown error while attempting to sign you In. Please try again later.`,
|
||||
),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
toast({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
description: _(
|
||||
msg`We encountered an unknown error while attempting update your profile. Please try again later.`,
|
||||
),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,19 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import type { User } from '@prisma/client';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import profileClaimTeaserImage from '@documenso/assets/images/profile-claim-teaser.png';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -35,7 +31,7 @@ import {
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
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({
|
||||
url: z
|
||||
@ -92,12 +88,12 @@ export const ClaimPublicProfileDialogForm = ({
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
if (error.code === AppErrorCode.PROFILE_URL_TAKEN) {
|
||||
if (error.code === 'PROFILE_URL_TAKEN') {
|
||||
form.setError('url', {
|
||||
type: 'manual',
|
||||
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', {
|
||||
type: 'manual',
|
||||
message: error.message,
|
||||
@ -135,7 +131,7 @@ export const ClaimPublicProfileDialogForm = ({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Image src={profileClaimTeaserImage} alt="profile claim teaser" />
|
||||
<img src={profileClaimTeaserImage} alt="profile claim teaser" />
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
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 { Plural, Trans } from '@lingui/react/macro';
|
||||
import type { TeamProfile, UserProfile } from '@prisma/client';
|
||||
import { motion } from 'framer-motion';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { CheckSquareIcon, CopyIcon } from 'lucide-react';
|
||||
@ -12,9 +12,8 @@ import { useForm } from 'react-hook-form';
|
||||
import type { z } from 'zod';
|
||||
|
||||
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 type { TeamProfile, UserProfile } from '@documenso/prisma/client';
|
||||
import {
|
||||
MAX_PROFILE_BIO_LENGTH,
|
||||
ZUpdatePublicProfileMutationSchema,
|
||||
@ -90,8 +89,8 @@ export const PublicProfileForm = ({
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
switch (error.code) {
|
||||
case AppErrorCode.PREMIUM_PROFILE_URL:
|
||||
case AppErrorCode.PROFILE_URL_TAKEN:
|
||||
case 'PREMIUM_PROFILE_URL':
|
||||
case 'PROFILE_URL_TAKEN':
|
||||
form.setError('url', {
|
||||
type: 'manual',
|
||||
message: error.message,
|
||||
@ -1,16 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { authClient } from '@documenso/auth/client';
|
||||
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 { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -43,7 +41,7 @@ export type ResetPasswordFormProps = {
|
||||
};
|
||||
|
||||
export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps) => {
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
@ -58,15 +56,15 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps)
|
||||
|
||||
const isSubmitting = form.formState.isSubmitting;
|
||||
|
||||
const { mutateAsync: resetPassword } = trpc.profile.resetPassword.useMutation();
|
||||
|
||||
const onFormSubmit = async ({ password }: Omit<TResetPasswordFormSchema, 'repeatedPassword'>) => {
|
||||
try {
|
||||
await resetPassword({
|
||||
await authClient.emailPassword.resetPassword({
|
||||
password,
|
||||
token,
|
||||
});
|
||||
|
||||
await navigate('/signin');
|
||||
|
||||
form.reset();
|
||||
|
||||
toast({
|
||||
@ -74,8 +72,6 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps)
|
||||
description: _(msg`Your password has been updated successfully.`),
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
router.push('/signin');
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
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';
|
||||
|
||||
@ -11,10 +12,10 @@ export type SearchParamSelector = {
|
||||
};
|
||||
|
||||
export const SearchParamSelector = ({ children, paramKey, isValueValid }: SearchParamSelector) => {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const { pathname } = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const value = useMemo(() => {
|
||||
const p = searchParams?.get(paramKey) ?? 'all';
|
||||
@ -35,7 +36,7 @@ export const SearchParamSelector = ({ children, paramKey, isValueValid }: Search
|
||||
params.delete(paramKey);
|
||||
}
|
||||
|
||||
router.push(`${pathname}?${params.toString()}`, { scroll: false });
|
||||
void navigate(`${pathname}?${params.toString()}`, { preventScrollReset: true });
|
||||
};
|
||||
|
||||
return (
|
||||
@ -1,12 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { authClient } from '@documenso/auth/client';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
@ -43,11 +42,9 @@ export const SendConfirmationEmailForm = ({ className }: SendConfirmationEmailFo
|
||||
|
||||
const isSubmitting = form.formState.isSubmitting;
|
||||
|
||||
const { mutateAsync: sendConfirmationEmail } = trpc.profile.sendConfirmationEmail.useMutation();
|
||||
|
||||
const onFormSubmit = async ({ email }: TSendConfirmationEmailFormSchema) => {
|
||||
try {
|
||||
await sendConfirmationEmail({ email });
|
||||
await authClient.emailPassword.resendVerifyEmail({ email });
|
||||
|
||||
toast({
|
||||
title: _(msg`Confirmation email sent`),
|
||||
@ -60,6 +57,7 @@ export const SendConfirmationEmailForm = ({ className }: SendConfirmationEmailFo
|
||||
form.reset();
|
||||
} catch (err) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: _(msg`An error occurred while sending your confirmation email`),
|
||||
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 Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
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 { Trans } from '@lingui/react/macro';
|
||||
import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser';
|
||||
import { KeyRoundIcon } from 'lucide-react';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { FaIdCardClip } from 'react-icons/fa6';
|
||||
import { FcGoogle } from 'react-icons/fc';
|
||||
import { Link, useNavigate } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { ErrorCode, isErrorCode } from '@documenso/lib/next-auth/error-codes';
|
||||
import { authClient } from '@documenso/auth/client';
|
||||
import { AuthenticationErrorCode } from '@documenso/auth/server/lib/errors/error-codes';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { ZCurrentPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
||||
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 { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
const ERROR_MESSAGES: Partial<Record<keyof typeof ErrorCode, string>> = {
|
||||
[ErrorCode.CREDENTIALS_NOT_FOUND]: 'The email or password provided is incorrect',
|
||||
[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 CommonErrorMessages: Record<string, MessageDescriptor> = {
|
||||
[AuthenticationErrorCode.AccountDisabled]: msg`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';
|
||||
|
||||
@ -88,9 +85,8 @@ export const SignInForm = ({
|
||||
}: SignInFormProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { getFlag } = useFeatureFlags();
|
||||
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] =
|
||||
useState(false);
|
||||
@ -101,9 +97,7 @@ export const SignInForm = ({
|
||||
|
||||
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
|
||||
|
||||
const isPasskeyEnabled = getFlag('app_passkey');
|
||||
|
||||
const callbackUrl = useMemo(() => {
|
||||
const redirectPath = useMemo(() => {
|
||||
// Handle SSR
|
||||
if (typeof window === 'undefined') {
|
||||
return LOGIN_REDIRECT_PATH;
|
||||
@ -170,25 +164,20 @@ export const SignInForm = ({
|
||||
try {
|
||||
setIsPasskeyLoading(true);
|
||||
|
||||
const options = await createPasskeySigninOptions();
|
||||
const { options, sessionId } = await createPasskeySigninOptions();
|
||||
|
||||
const credential = await startAuthentication(options);
|
||||
|
||||
const result = await signIn('webauthn', {
|
||||
await authClient.passkey.signIn({
|
||||
credential: JSON.stringify(credential),
|
||||
callbackUrl,
|
||||
redirect: false,
|
||||
csrfToken: sessionId,
|
||||
redirectPath,
|
||||
});
|
||||
|
||||
if (!result?.url || result.error) {
|
||||
throw new AppError(result?.error ?? '');
|
||||
}
|
||||
|
||||
window.location.href = result.url;
|
||||
} catch (err) {
|
||||
setIsPasskeyLoading(false);
|
||||
|
||||
if (err.name === 'NotAllowedError') {
|
||||
// Error from library.
|
||||
if (err instanceof Error && err.name === 'NotAllowedError') {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -196,12 +185,15 @@ export const SignInForm = ({
|
||||
|
||||
const errorMessage = match(error.code)
|
||||
.with(
|
||||
AppErrorCode.NOT_SETUP,
|
||||
AuthenticationErrorCode.NotSetup,
|
||||
() =>
|
||||
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.`)
|
||||
.otherwise(() => msg`Please try again later or login using your normal details`);
|
||||
.with(
|
||||
AuthenticationErrorCode.SessionExpired,
|
||||
() => msg`This session has expired. Please try again.`,
|
||||
)
|
||||
.otherwise(() => handleFallbackErrorMessages(error.code));
|
||||
|
||||
toast({
|
||||
title: 'Something went wrong',
|
||||
@ -214,73 +206,58 @@ export const SignInForm = ({
|
||||
|
||||
const onFormSubmit = async ({ email, password, totpCode, backupCode }: TSignInFormSchema) => {
|
||||
try {
|
||||
const credentials: Record<string, string> = {
|
||||
await authClient.emailPassword.signIn({
|
||||
email,
|
||||
password,
|
||||
};
|
||||
|
||||
if (totpCode) {
|
||||
credentials.totpCode = totpCode;
|
||||
}
|
||||
|
||||
if (backupCode) {
|
||||
credentials.backupCode = backupCode;
|
||||
}
|
||||
|
||||
const result = await signIn('credentials', {
|
||||
...credentials,
|
||||
callbackUrl,
|
||||
redirect: false,
|
||||
totpCode,
|
||||
backupCode,
|
||||
redirectPath,
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
|
||||
if (result?.error && isErrorCode(result.error)) {
|
||||
if (result.error === TwoFactorEnabledErrorCode) {
|
||||
setIsTwoFactorAuthenticationDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
const errorMessage = ERROR_MESSAGES[result.error];
|
||||
if (error.code === 'TWO_FACTOR_MISSING_CREDENTIALS') {
|
||||
setIsTwoFactorAuthenticationDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.error === ErrorCode.UNVERIFIED_EMAIL) {
|
||||
router.push(`/unverified-account`);
|
||||
|
||||
toast({
|
||||
title: _(msg`Unable to sign in`),
|
||||
description: errorMessage ?? _(msg`An unknown error occurred`),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
if (error.code === AuthenticationErrorCode.UnverifiedEmail) {
|
||||
await navigate('/unverified-account');
|
||||
|
||||
toast({
|
||||
title: _(msg`Unable to sign in`),
|
||||
description: errorMessage ?? _(msg`An unknown error occurred`),
|
||||
variant: 'destructive',
|
||||
description: _(
|
||||
msg`This account has not been verified. Please verify your account before signing in.`,
|
||||
),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result?.url) {
|
||||
throw new Error('An unknown error occurred');
|
||||
}
|
||||
const errorMessage = match(error.code)
|
||||
.with(
|
||||
AuthenticationErrorCode.InvalidCredentials,
|
||||
() => msg`The email or password provided is incorrect`,
|
||||
)
|
||||
.with(
|
||||
AuthenticationErrorCode.InvalidTwoFactorCode,
|
||||
() => msg`The two-factor authentication code provided is incorrect`,
|
||||
)
|
||||
.otherwise(() => handleFallbackErrorMessages(error.code));
|
||||
|
||||
window.location.href = result.url;
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
description: _(
|
||||
msg`We encountered an unknown error while attempting to sign you In. Please try again later.`,
|
||||
),
|
||||
title: _(msg`Unable to sign in`),
|
||||
description: _(errorMessage),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onSignInWithGoogleClick = async () => {
|
||||
try {
|
||||
await signIn('google', {
|
||||
callbackUrl,
|
||||
});
|
||||
await authClient.google.signIn();
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
@ -294,9 +271,11 @@ export const SignInForm = ({
|
||||
|
||||
const onSignInWithOIDCClick = async () => {
|
||||
try {
|
||||
await signIn('oidc', {
|
||||
callbackUrl,
|
||||
});
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
// await signIn('oidc', {
|
||||
// callbackUrl,
|
||||
// });
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
@ -365,7 +344,7 @@ export const SignInForm = ({
|
||||
|
||||
<p className="mt-2 text-right">
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
to="/forgot-password"
|
||||
className="text-muted-foreground text-sm duration-200 hover:opacity-70"
|
||||
>
|
||||
<Trans>Forgot your password?</Trans>
|
||||
@ -384,7 +363,7 @@ export const SignInForm = ({
|
||||
{isSubmitting ? <Trans>Signing in...</Trans> : <Trans>Sign In</Trans>}
|
||||
</Button>
|
||||
|
||||
{(isGoogleSSOEnabled || isPasskeyEnabled || isOIDCSSOEnabled) && (
|
||||
{(isGoogleSSOEnabled || isOIDCSSOEnabled) && (
|
||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||
<div className="bg-border h-px flex-1" />
|
||||
<span className="text-muted-foreground bg-transparent">
|
||||
@ -422,20 +401,18 @@ export const SignInForm = ({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isPasskeyEnabled && (
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
variant="outline"
|
||||
disabled={isSubmitting}
|
||||
loading={isPasskeyLoading}
|
||||
className="bg-background text-muted-foreground border"
|
||||
onClick={onSignInWithPasskey}
|
||||
>
|
||||
{!isPasskeyLoading && <KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />}
|
||||
<Trans>Passkey</Trans>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
variant="outline"
|
||||
disabled={isSubmitting}
|
||||
loading={isPasskeyLoading}
|
||||
className="bg-background text-muted-foreground border"
|
||||
onClick={onSignInWithPasskey}
|
||||
>
|
||||
{!isPasskeyLoading && <KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />}
|
||||
<Trans>Passkey</Trans>
|
||||
</Button>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
@ -1,27 +1,22 @@
|
||||
'use client';
|
||||
|
||||
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 type { MessageDescriptor } from '@lingui/core';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { FaIdCardClip } from 'react-icons/fa6';
|
||||
import { FcGoogle } from 'react-icons/fc';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
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 { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
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 { cn } from '@documenso/ui/lib/utils';
|
||||
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 { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { UserProfileSkeleton } from '~/components/ui/user-profile-skeleton';
|
||||
import { UserProfileTimur } from '~/components/ui/user-profile-timur';
|
||||
import { UserProfileSkeleton } from '~/components/general/user-profile-skeleton';
|
||||
import { UserProfileTimur } from '~/components/general/user-profile-timur';
|
||||
|
||||
const SIGN_UP_REDIRECT_PATH = '/documents';
|
||||
|
||||
type SignUpStep = 'BASIC_DETAILS' | 'CLAIM_USERNAME';
|
||||
|
||||
export const ZSignUpFormV2Schema = z
|
||||
export const ZSignUpFormSchema = z
|
||||
.object({
|
||||
name: z
|
||||
.string()
|
||||
@ -78,39 +73,39 @@ export const signupErrorMessages: Record<string, MessageDescriptor> = {
|
||||
SIGNUP_DISABLED: msg`Signups are disabled.`,
|
||||
[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.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`,
|
||||
PROFILE_URL_TAKEN: msg`This username has already been taken`,
|
||||
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;
|
||||
initialEmail?: string;
|
||||
isGoogleSSOEnabled?: boolean;
|
||||
isOIDCSSOEnabled?: boolean;
|
||||
};
|
||||
|
||||
export const SignUpFormV2 = ({
|
||||
export const SignUpForm = ({
|
||||
className,
|
||||
initialEmail,
|
||||
isGoogleSSOEnabled,
|
||||
isOIDCSSOEnabled,
|
||||
}: SignUpFormV2Props) => {
|
||||
}: SignUpFormProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const analytics = useAnalytics();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
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 form = useForm<TSignUpFormV2Schema>({
|
||||
const form = useForm<TSignUpFormSchema>({
|
||||
values: {
|
||||
name: '',
|
||||
email: initialEmail ?? '',
|
||||
@ -119,7 +114,7 @@ export const SignUpFormV2 = ({
|
||||
url: '',
|
||||
},
|
||||
mode: 'onBlur',
|
||||
resolver: zodResolver(ZSignUpFormV2Schema),
|
||||
resolver: zodResolver(ZSignUpFormSchema),
|
||||
});
|
||||
|
||||
const isSubmitting = form.formState.isSubmitting;
|
||||
@ -127,13 +122,17 @@ export const SignUpFormV2 = ({
|
||||
const name = form.watch('name');
|
||||
const url = form.watch('url');
|
||||
|
||||
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
|
||||
|
||||
const onFormSubmit = async ({ name, email, password, signature, url }: TSignUpFormV2Schema) => {
|
||||
const onFormSubmit = async ({ name, email, password, signature, url }: TSignUpFormSchema) => {
|
||||
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({
|
||||
title: _(msg`Registration Successful`),
|
||||
@ -153,10 +152,7 @@ export const SignUpFormV2 = ({
|
||||
|
||||
const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST;
|
||||
|
||||
if (
|
||||
error.code === AppErrorCode.PROFILE_URL_TAKEN ||
|
||||
error.code === AppErrorCode.PREMIUM_PROFILE_URL
|
||||
) {
|
||||
if (error.code === 'PROFILE_URL_TAKEN' || error.code === 'PREMIUM_PROFILE_URL') {
|
||||
form.setError('url', {
|
||||
type: 'manual',
|
||||
message: _(errorMessage),
|
||||
@ -181,7 +177,7 @@ export const SignUpFormV2 = ({
|
||||
|
||||
const onSignUpWithGoogleClick = async () => {
|
||||
try {
|
||||
await signIn('google', { callbackUrl: SIGN_UP_REDIRECT_PATH });
|
||||
await authClient.google.signIn();
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
@ -195,7 +191,9 @@ export const SignUpFormV2 = ({
|
||||
|
||||
const onSignUpWithOIDCClick = async () => {
|
||||
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) {
|
||||
toast({
|
||||
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="border-border relative hidden flex-1 overflow-hidden rounded-xl border xl:flex">
|
||||
<div className="absolute -inset-8 -z-[2] backdrop-blur">
|
||||
<Image
|
||||
<img
|
||||
src={communityCardsImage}
|
||||
fill={true}
|
||||
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>
|
||||
|
||||
@ -426,10 +423,7 @@ export const SignUpFormV2 = ({
|
||||
<p className="text-muted-foreground mt-4 text-sm">
|
||||
<Trans>
|
||||
Already have an account?{' '}
|
||||
<Link
|
||||
href="/signin"
|
||||
className="text-documenso-700 duration-200 hover:opacity-70"
|
||||
>
|
||||
<Link to="/signin" className="text-documenso-700 duration-200 hover:opacity-70">
|
||||
Sign in instead
|
||||
</Link>
|
||||
</Trans>
|
||||
@ -1,17 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { Team, TeamGlobalSettings } from '@prisma/client';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getFile } from '@documenso/lib/universal/upload/get-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 { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -1,19 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 { z } from 'zod';
|
||||
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import {
|
||||
SUPPORTED_LANGUAGES,
|
||||
SUPPORTED_LANGUAGE_CODES,
|
||||
isValidLanguageCode,
|
||||
} 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 { Alert } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -56,9 +55,9 @@ export const TeamDocumentPreferencesForm = ({
|
||||
}: TeamDocumentPreferencesFormProps) => {
|
||||
const { _ } = useLingui();
|
||||
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 } =
|
||||
trpc.team.updateTeamDocumentSettings.useMutation();
|
||||
@ -1,15 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router';
|
||||
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 { trpc } from '@documenso/trpc/react';
|
||||
import { ZUpdateTeamMutationSchema } from '@documenso/trpc/server/team-router/schema';
|
||||
@ -31,20 +29,20 @@ export type UpdateTeamDialogProps = {
|
||||
teamUrl: string;
|
||||
};
|
||||
|
||||
const ZUpdateTeamFormSchema = ZUpdateTeamMutationSchema.shape.data.pick({
|
||||
const ZTeamUpdateFormSchema = ZUpdateTeamMutationSchema.shape.data.pick({
|
||||
name: true,
|
||||
url: true,
|
||||
});
|
||||
|
||||
type TUpdateTeamFormSchema = z.infer<typeof ZUpdateTeamFormSchema>;
|
||||
type TTeamUpdateFormSchema = z.infer<typeof ZTeamUpdateFormSchema>;
|
||||
|
||||
export const UpdateTeamForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogProps) => {
|
||||
const router = useRouter();
|
||||
export const TeamUpdateForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(ZUpdateTeamFormSchema),
|
||||
resolver: zodResolver(ZTeamUpdateFormSchema),
|
||||
defaultValues: {
|
||||
name: teamName,
|
||||
url: teamUrl,
|
||||
@ -53,7 +51,7 @@ export const UpdateTeamForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogPr
|
||||
|
||||
const { mutateAsync: updateTeam } = trpc.team.updateTeam.useMutation();
|
||||
|
||||
const onFormSubmit = async ({ name, url }: TUpdateTeamFormSchema) => {
|
||||
const onFormSubmit = async ({ name, url }: TTeamUpdateFormSchema) => {
|
||||
try {
|
||||
await updateTeam({
|
||||
data: {
|
||||
@ -75,7 +73,7 @@ export const UpdateTeamForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogPr
|
||||
});
|
||||
|
||||
if (url !== teamUrl) {
|
||||
router.push(`${WEBAPP_BASE_URL}/t/${url}/settings`);
|
||||
await navigate(`/t/${url}/settings`);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
@ -133,7 +131,7 @@ export const UpdateTeamForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogPr
|
||||
{!form.formState.errors.url && (
|
||||
<span className="text-foreground/50 text-xs font-normal">
|
||||
{field.value ? (
|
||||
`${WEBAPP_BASE_URL}/t/${field.value}`
|
||||
`${NEXT_PUBLIC_WEBAPP_URL()}/t/${field.value}`
|
||||
) : (
|
||||
<Trans>A unique URL to identify your team</Trans>
|
||||
)}
|
||||
@ -1,19 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { ApiToken } from '@prisma/client';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
||||
import type { ApiToken } from '@documenso/prisma/client';
|
||||
import { TRPCClientError } from '@documenso/trpc/client';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TCreateTokenMutationSchema } 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 { 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({
|
||||
enabled: z.boolean(),
|
||||
@ -60,7 +64,6 @@ export type ApiTokenFormProps = {
|
||||
};
|
||||
|
||||
export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) => {
|
||||
const router = useRouter();
|
||||
const [isTransitionPending, startTransition] = useTransition();
|
||||
|
||||
const [, copy] = useCopyToClipboard();
|
||||
@ -71,13 +74,6 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
|
||||
const [newlyCreatedToken, setNewlyCreatedToken] = useState<NewlyCreatedToken | null>();
|
||||
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({
|
||||
onSuccess(data) {
|
||||
setNewlyCreatedToken(data);
|
||||
@ -129,25 +125,22 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
|
||||
});
|
||||
|
||||
form.reset();
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
startTransition(() => router.refresh());
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCClientError && error.data?.code === 'BAD_REQUEST') {
|
||||
toast({
|
||||
title: _(msg`An error occurred`),
|
||||
description: error.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
description: _(
|
||||
msg`We encountered an unknown error while attempting create the new token. Please try again later.`,
|
||||
),
|
||||
variant: 'destructive',
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
const errorMessage = match(error.code)
|
||||
.with(
|
||||
AppErrorCode.UNAUTHORIZED,
|
||||
() => msg`You do not have permission to create a token for this team`,
|
||||
)
|
||||
.otherwise(() => msg`Something went wrong. Please try again later.`);
|
||||
|
||||
toast({
|
||||
title: _(msg`An error occurred`),
|
||||
description: _(errorMessage),
|
||||
variant: 'destructive',
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -263,34 +256,36 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
<AnimatePresence initial={!hasNewlyCreatedToken}>
|
||||
{newlyCreatedToken && hasNewlyCreatedToken && (
|
||||
<motion.div
|
||||
className="mt-8"
|
||||
initial={{ opacity: 0, y: -40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 40 }}
|
||||
>
|
||||
<Card gradient>
|
||||
<CardContent className="p-4">
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
<Trans>
|
||||
Your token was created successfully! Make sure to copy it because you won't be
|
||||
able to see it again!
|
||||
</Trans>
|
||||
</p>
|
||||
<AnimatePresence>
|
||||
{newlyCreatedToken &&
|
||||
tokens &&
|
||||
tokens.find((token) => token.id === newlyCreatedToken.id) && (
|
||||
<motion.div
|
||||
className="mt-8"
|
||||
initial={{ opacity: 0, y: -40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 40 }}
|
||||
>
|
||||
<Card gradient>
|
||||
<CardContent className="p-4">
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
<Trans>
|
||||
Your token was created successfully! Make sure to copy it because you won't be
|
||||
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">
|
||||
{newlyCreatedToken.token}
|
||||
</p>
|
||||
<p className="bg-muted-foreground/10 my-4 rounded-md px-2.5 py-1 font-mono text-sm">
|
||||
{newlyCreatedToken.token}
|
||||
</p>
|
||||
|
||||
<Button variant="outline" onClick={() => void copyToken(newlyCreatedToken.token)}>
|
||||
<Trans>Copy token</Trans>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
<Button variant="outline" onClick={() => void copyToken(newlyCreatedToken.token)}>
|
||||
<Trans>Copy token</Trans>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
@ -1,23 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import { DateTime } from 'luxon';
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
import type { GetSignerConversionMonthlyResult } from '@documenso/lib/server-only/user/get-signer-conversion';
|
||||
|
||||
export type SignerConversionChartProps = {
|
||||
export type AdminStatsSignerConversionChartProps = {
|
||||
className?: string;
|
||||
title: string;
|
||||
cummulative?: boolean;
|
||||
data: GetSignerConversionMonthlyResult;
|
||||
};
|
||||
|
||||
export const SignerConversionChart = ({
|
||||
export const AdminStatsSignerConversionChart = ({
|
||||
className,
|
||||
data,
|
||||
title,
|
||||
cummulative = false,
|
||||
}: SignerConversionChartProps) => {
|
||||
}: AdminStatsSignerConversionChartProps) => {
|
||||
const formattedData = [...data].reverse().map(({ month, count, cume_count }) => {
|
||||
return {
|
||||
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('MMM yyyy'),
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import { DateTime } from 'luxon';
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } 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';
|
||||
|
||||
export type UserWithDocumentChartProps = {
|
||||
export type AdminStatsUsersWithDocumentsChartProps = {
|
||||
className?: string;
|
||||
title: string;
|
||||
data: GetUserWithDocumentMonthlyGrowth;
|
||||
@ -23,7 +21,7 @@ const CustomTooltip = ({
|
||||
}: TooltipProps<ValueType, NameType> & { tooltip?: string }) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="z-100 w-60 space-y-1 rounded-md border border-solid bg-white p-2 px-3">
|
||||
<div className="z-100 w-60 space-y-1 rounded-md border border-solid bg-white p-2 px-3">
|
||||
<p className="">{label}</p>
|
||||
<p className="text-documenso">
|
||||
{`${tooltip} : `}
|
||||
@ -36,13 +34,13 @@ const CustomTooltip = ({
|
||||
return null;
|
||||
};
|
||||
|
||||
export const UserWithDocumentChart = ({
|
||||
export const AdminStatsUsersWithDocumentsChart = ({
|
||||
className,
|
||||
data,
|
||||
title,
|
||||
completed = false,
|
||||
tooltip,
|
||||
}: UserWithDocumentChartProps) => {
|
||||
}: AdminStatsUsersWithDocumentsChartProps) => {
|
||||
const formattedData = (data: GetUserWithDocumentMonthlyGrowth, completed: boolean) => {
|
||||
return [...data].reverse().map(({ month, count, signed_count }) => {
|
||||
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 { useRouter } from 'next/navigation';
|
||||
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { CheckIcon, Loader, Monitor, Moon, Sun } from 'lucide-react';
|
||||
import { useTheme } from 'next-themes';
|
||||
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 {
|
||||
@ -21,7 +19,6 @@ import {
|
||||
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
SKIP_QUERY_BATCH_META,
|
||||
} from '@documenso/lib/constants/trpc';
|
||||
import { switchI18NLanguage } from '@documenso/lib/server-only/i18n/switch-i18n-language';
|
||||
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@ -34,7 +31,6 @@ import {
|
||||
CommandList,
|
||||
CommandShortcut,
|
||||
} from '@documenso/ui/primitives/command';
|
||||
import { THEMES_TYPE } from '@documenso/ui/primitives/constants';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
const DOCUMENTS_PAGES = [
|
||||
@ -70,16 +66,15 @@ const SETTINGS_PAGES = [
|
||||
{ label: msg`Password`, path: '/settings/password' },
|
||||
];
|
||||
|
||||
export type CommandMenuProps = {
|
||||
export type AppCommandMenuProps = {
|
||||
open?: boolean;
|
||||
onOpenChange?: (_open: boolean) => void;
|
||||
};
|
||||
|
||||
export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
||||
export function AppCommandMenu({ open, onOpenChange }: AppCommandMenuProps) {
|
||||
const { _ } = useLingui();
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(() => open ?? false);
|
||||
const [search, setSearch] = useState('');
|
||||
@ -91,7 +86,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
||||
query: search,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
placeholderData: (previousData) => previousData,
|
||||
// Do not batch this due to relatively long request time compared to
|
||||
// other queries which are generally batched with this.
|
||||
...SKIP_QUERY_BATCH_META,
|
||||
@ -138,10 +133,10 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
||||
|
||||
const push = useCallback(
|
||||
(path: string) => {
|
||||
router.push(path);
|
||||
void navigate(path);
|
||||
setOpen(false);
|
||||
},
|
||||
[router, setOpen],
|
||||
[setOpen],
|
||||
);
|
||||
|
||||
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 />}
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
@ -256,19 +251,18 @@ const Commands = ({
|
||||
));
|
||||
};
|
||||
|
||||
const ThemeCommands = ({ setTheme }: { setTheme: (_theme: string) => void }) => {
|
||||
const ThemeCommands = () => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const THEMES = useMemo(
|
||||
() => [
|
||||
{ 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 },
|
||||
],
|
||||
[],
|
||||
);
|
||||
const [, setTheme] = useTheme();
|
||||
|
||||
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
|
||||
key={theme.theme}
|
||||
onSelect={() => setTheme(theme.theme)}
|
||||
@ -294,9 +288,23 @@ const LanguageCommands = () => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await dynamicActivate(i18n, lang);
|
||||
await switchI18NLanguage(lang);
|
||||
} catch (err) {
|
||||
await dynamicActivate(lang);
|
||||
|
||||
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({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
variant: 'destructive',
|
||||
@ -1,32 +1,28 @@
|
||||
'use client';
|
||||
|
||||
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 { 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 { getRootHref } from '@documenso/lib/utils/params';
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
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 { DesktopNav } from './desktop-nav';
|
||||
import { AppCommandMenu } from './app-command-menu';
|
||||
import { AppNavDesktop } from './app-nav-desktop';
|
||||
import { AppNavMobile } from './app-nav-mobile';
|
||||
import { MenuSwitcher } from './menu-switcher';
|
||||
import { MobileNavigation } from './mobile-navigation';
|
||||
|
||||
export type HeaderProps = HTMLAttributes<HTMLDivElement> & {
|
||||
user: User;
|
||||
user: SessionUser;
|
||||
teams: TGetTeamsResponse;
|
||||
};
|
||||
|
||||
export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
||||
const params = useParams();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const [isCommandMenuOpen, setIsCommandMenuOpen] = useState(false);
|
||||
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
|
||||
@ -42,8 +38,6 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
const isPathTeamUrl = (teamUrl: string) => {
|
||||
if (!pathname || !pathname.startsWith(`/t/`)) {
|
||||
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">
|
||||
<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"
|
||||
>
|
||||
<Logo className="h-6 w-auto" />
|
||||
<BrandingLogo className="h-6 w-auto" />
|
||||
</Link>
|
||||
|
||||
<DesktopNav setIsCommandMenuOpen={setIsCommandMenuOpen} />
|
||||
<AppNavDesktop setIsCommandMenuOpen={setIsCommandMenuOpen} />
|
||||
|
||||
<div
|
||||
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" />
|
||||
</button>
|
||||
|
||||
<CommandMenu open={isCommandMenuOpen} onOpenChange={setIsCommandMenuOpen} />
|
||||
<AppCommandMenu open={isCommandMenuOpen} onOpenChange={setIsCommandMenuOpen} />
|
||||
|
||||
<MobileNavigation
|
||||
<AppNavMobile
|
||||
isMenuOpen={isHamburgerMenuOpen}
|
||||
onMenuOpenChange={setIsHamburgerMenuOpen}
|
||||
/>
|
||||
@ -1,12 +1,11 @@
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useParams, usePathname } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Search } from 'lucide-react';
|
||||
import { Link, useLocation, useParams } from 'react-router';
|
||||
|
||||
import { getRootHref } from '@documenso/lib/utils/params';
|
||||
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;
|
||||
};
|
||||
|
||||
export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: DesktopNavProps) => {
|
||||
export const AppNavDesktop = ({
|
||||
className,
|
||||
setIsCommandMenuOpen,
|
||||
...props
|
||||
}: AppNavDesktopProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const pathname = usePathname();
|
||||
const { pathname } = useLocation();
|
||||
const params = useParams();
|
||||
|
||||
const [modifierKey, setModifierKey] = useState(() => 'Ctrl');
|
||||
@ -56,7 +59,7 @@ export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: Deskto
|
||||
{navigationLinks.map(({ href, label }) => (
|
||||
<Link
|
||||
key={href}
|
||||
href={`${rootHref}${href}`}
|
||||
to={`${rootHref}${href}`}
|
||||
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',
|
||||
{
|
||||
@ -82,7 +85,7 @@ export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: Deskto
|
||||
</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
|
||||
</div>
|
||||
</div>
|
||||
@ -1,24 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 { authClient } from '@documenso/auth/client';
|
||||
import { getRootHref } from '@documenso/lib/utils/params';
|
||||
import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet';
|
||||
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
|
||||
|
||||
export type MobileNavigationProps = {
|
||||
export type AppNavMobileProps = {
|
||||
isMenuOpen: boolean;
|
||||
onMenuOpenChange?: (_value: boolean) => void;
|
||||
};
|
||||
|
||||
export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigationProps) => {
|
||||
export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const params = useParams();
|
||||
@ -51,8 +47,8 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
||||
return (
|
||||
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
|
||||
<SheetContent className="flex w-full max-w-[350px] flex-col">
|
||||
<Link href="/" onClick={handleMenuItemClick}>
|
||||
<Image
|
||||
<Link to="/" onClick={handleMenuItemClick}>
|
||||
<img
|
||||
src={LogoImage}
|
||||
alt="Documenso Logo"
|
||||
className="dark:invert"
|
||||
@ -66,7 +62,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
||||
<Link
|
||||
key={href}
|
||||
className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
|
||||
href={href}
|
||||
to={href}
|
||||
onClick={() => handleMenuItemClick()}
|
||||
>
|
||||
{_(text)}
|
||||
@ -75,11 +71,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
||||
|
||||
<button
|
||||
className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
|
||||
onClick={async () =>
|
||||
signOut({
|
||||
callbackUrl: '/',
|
||||
})
|
||||
}
|
||||
onClick={async () => authClient.signOut()}
|
||||
>
|
||||
<Trans>Sign Out</Trans>
|
||||
</button>
|
||||
@ -1,17 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
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 { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
@ -2,7 +2,7 @@ import type { SVGAttributes } from 'react';
|
||||
|
||||
export type LogoProps = SVGAttributes<SVGSVGElement>;
|
||||
|
||||
export const Logo = ({ ...props }: LogoProps) => {
|
||||
export const BrandingLogo = ({ ...props }: LogoProps) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2248 320" {...props}>
|
||||
<path
|
||||
@ -1,16 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { authClient } from '@documenso/auth/client';
|
||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
@ -25,7 +23,7 @@ import { Input } from '@documenso/ui/primitives/input';
|
||||
import { PasswordInput } from '@documenso/ui/primitives/password-input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { signupErrorMessages } from '~/components/forms/v2/signup';
|
||||
import { signupErrorMessages } from '~/components/forms/signup';
|
||||
|
||||
export type ClaimAccountProps = {
|
||||
defaultName: string;
|
||||
@ -60,9 +58,7 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
|
||||
const { toast } = useToast();
|
||||
|
||||
const analytics = useAnalytics();
|
||||
const router = useRouter();
|
||||
|
||||
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const form = useForm<TClaimAccountFormSchema>({
|
||||
values: {
|
||||
@ -75,9 +71,9 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
|
||||
|
||||
const onFormSubmit = async ({ name, email, password }: TClaimAccountFormSchema) => {
|
||||
try {
|
||||
await signup({ name, email, password });
|
||||
await authClient.emailPassword.signUp({ name, email, password });
|
||||
|
||||
router.push(`/unverified-account`);
|
||||
await navigate(`/unverified-account`);
|
||||
|
||||
toast({
|
||||
title: _(msg`Registration Successful`),
|
||||
@ -1,14 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 { z } from 'zod';
|
||||
|
||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||
import type { TemplateWithDetails } from '@documenso/prisma/types/template';
|
||||
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
||||
import type { TTemplate } from '@documenso/lib/types/template';
|
||||
import {
|
||||
DocumentFlowFormContainerActions,
|
||||
DocumentFlowFormContainerContent,
|
||||
@ -29,38 +29,39 @@ import {
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
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'),
|
||||
});
|
||||
|
||||
export type TConfigureDirectTemplateFormSchema = z.infer<typeof ZConfigureDirectTemplateFormSchema>;
|
||||
export type TDirectTemplateConfigureFormSchema = z.infer<typeof ZDirectTemplateConfigureFormSchema>;
|
||||
|
||||
export type ConfigureDirectTemplateFormProps = {
|
||||
export type DirectTemplateConfigureFormProps = {
|
||||
flowStep: DocumentFlowStep;
|
||||
isDocumentPdfLoaded: boolean;
|
||||
template: Omit<TemplateWithDetails, 'User'>;
|
||||
directTemplateRecipient: Recipient & { Field: Field[] };
|
||||
template: Omit<TTemplate, 'user'>;
|
||||
directTemplateRecipient: Recipient & { fields: Field[] };
|
||||
initialEmail?: string;
|
||||
onSubmit: (_data: TConfigureDirectTemplateFormSchema) => void;
|
||||
onSubmit: (_data: TDirectTemplateConfigureFormSchema) => void;
|
||||
};
|
||||
|
||||
export const ConfigureDirectTemplateFormPartial = ({
|
||||
export const DirectTemplateConfigureForm = ({
|
||||
flowStep,
|
||||
isDocumentPdfLoaded,
|
||||
template,
|
||||
directTemplateRecipient,
|
||||
initialEmail,
|
||||
onSubmit,
|
||||
}: ConfigureDirectTemplateFormProps) => {
|
||||
}: DirectTemplateConfigureFormProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { data: session } = useSession();
|
||||
|
||||
const { Recipient } = template;
|
||||
const { derivedRecipientAccessAuth } = useRequiredDocumentAuthContext();
|
||||
const { user } = useOptionalSession();
|
||||
|
||||
const recipientsWithBlankDirectRecipientEmail = Recipient.map((recipient) => {
|
||||
const { recipients } = template;
|
||||
const { derivedRecipientAccessAuth } = useRequiredDocumentSigningAuthContext();
|
||||
|
||||
const recipientsWithBlankDirectRecipientEmail = recipients.map((recipient) => {
|
||||
if (recipient.id === directTemplateRecipient.id) {
|
||||
return {
|
||||
...recipient,
|
||||
@ -71,10 +72,10 @@ export const ConfigureDirectTemplateFormPartial = ({
|
||||
return recipient;
|
||||
});
|
||||
|
||||
const form = useForm<TConfigureDirectTemplateFormSchema>({
|
||||
const form = useForm<TDirectTemplateConfigureFormSchema>({
|
||||
resolver: zodResolver(
|
||||
ZConfigureDirectTemplateFormSchema.superRefine((items, ctx) => {
|
||||
if (template.Recipient.map((recipient) => recipient.email).includes(items.email)) {
|
||||
ZDirectTemplateConfigureFormSchema.superRefine((items, ctx) => {
|
||||
if (template.recipients.map((recipient) => recipient.email).includes(items.email)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: _(msg`Email cannot already exist in the template`),
|
||||
@ -96,7 +97,7 @@ export const ConfigureDirectTemplateFormPartial = ({
|
||||
|
||||
<DocumentFlowFormContainerContent>
|
||||
{isDocumentPdfLoaded &&
|
||||
directTemplateRecipient.Field.map((field, index) => (
|
||||
directTemplateRecipient.fields.map((field, index) => (
|
||||
<ShowFieldItem
|
||||
key={index}
|
||||
field={field}
|
||||
@ -124,7 +125,7 @@ export const ConfigureDirectTemplateFormPartial = ({
|
||||
disabled={
|
||||
field.disabled ||
|
||||
derivedRecipientAccessAuth !== null ||
|
||||
session?.user.email !== undefined
|
||||
user?.email !== undefined
|
||||
}
|
||||
placeholder="recipient@documenso.com"
|
||||
/>
|
||||
@ -1,16 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 type { Field } from '@documenso/prisma/client';
|
||||
import { type Recipient } from '@documenso/prisma/client';
|
||||
import type { TemplateWithDetails } from '@documenso/prisma/types/template';
|
||||
import type { TTemplate } from '@documenso/lib/types/template';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
|
||||
@ -19,18 +16,22 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { Stepper } from '@documenso/ui/primitives/stepper';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useRequiredDocumentAuthContext } from '~/app/(signing)/sign/[token]/document-auth-provider';
|
||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
||||
import { useRequiredDocumentSigningAuthContext } from '~/components/general/document-signing/document-signing-auth-provider';
|
||||
import { useRequiredDocumentSigningContext } from '~/components/general/document-signing/document-signing-provider';
|
||||
|
||||
import type { TConfigureDirectTemplateFormSchema } from './configure-direct-template';
|
||||
import { ConfigureDirectTemplateFormPartial } from './configure-direct-template';
|
||||
import type { DirectTemplateLocalField } from './sign-direct-template';
|
||||
import { SignDirectTemplateForm } from './sign-direct-template';
|
||||
import {
|
||||
DirectTemplateConfigureForm,
|
||||
type TDirectTemplateConfigureFormSchema,
|
||||
} from './direct-template-configure-form';
|
||||
import {
|
||||
type DirectTemplateLocalField,
|
||||
DirectTemplateSigningForm,
|
||||
} from './direct-template-signing-form';
|
||||
|
||||
export type TemplatesDirectPageViewProps = {
|
||||
template: Omit<TemplateWithDetails, 'User'>;
|
||||
export type DirectTemplatePageViewProps = {
|
||||
template: Omit<TTemplate, 'user'>;
|
||||
directTemplateToken: string;
|
||||
directTemplateRecipient: Recipient & { Field: Field[] };
|
||||
directTemplateRecipient: Recipient & { fields: Field[] };
|
||||
};
|
||||
|
||||
type DirectTemplateStep = 'configure' | 'sign';
|
||||
@ -40,15 +41,15 @@ export const DirectTemplatePageView = ({
|
||||
template,
|
||||
directTemplateRecipient,
|
||||
directTemplateToken,
|
||||
}: TemplatesDirectPageViewProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
}: DirectTemplatePageViewProps) => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { email, fullName, setEmail } = useRequiredSigningContext();
|
||||
const { recipient, setRecipient } = useRequiredDocumentAuthContext();
|
||||
const { email, fullName, setEmail } = useRequiredDocumentSigningContext();
|
||||
const { recipient, setRecipient } = useRequiredDocumentSigningAuthContext();
|
||||
|
||||
const [step, setStep] = useState<DirectTemplateStep>('configure');
|
||||
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.
|
||||
*/
|
||||
const onConfigureDirectTemplateSubmit = ({ email }: TConfigureDirectTemplateFormSchema) => {
|
||||
const onConfigureDirectTemplateSubmit = ({ email }: TDirectTemplateConfigureFormSchema) => {
|
||||
setEmail(email);
|
||||
|
||||
setRecipient({
|
||||
@ -112,7 +113,7 @@ export const DirectTemplatePageView = ({
|
||||
|
||||
const redirectUrl = template.templateMeta?.redirectUrl;
|
||||
|
||||
redirectUrl ? router.push(redirectUrl) : router.push(`/sign/${token}/complete`);
|
||||
await (redirectUrl ? navigate(redirectUrl) : navigate(`/sign/${token}/complete`));
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
@ -152,7 +153,7 @@ export const DirectTemplatePageView = ({
|
||||
currentStep={currentDocumentFlow.stepIndex}
|
||||
setCurrentStep={(step) => setStep(DirectTemplateSteps[step - 1])}
|
||||
>
|
||||
<ConfigureDirectTemplateFormPartial
|
||||
<DirectTemplateConfigureForm
|
||||
flowStep={directTemplateFlow.configure}
|
||||
template={template}
|
||||
directTemplateRecipient={directTemplateRecipient}
|
||||
@ -161,10 +162,10 @@ export const DirectTemplatePageView = ({
|
||||
initialEmail={email}
|
||||
/>
|
||||
|
||||
<SignDirectTemplateForm
|
||||
<DirectTemplateSigningForm
|
||||
flowStep={directTemplateFlow.sign}
|
||||
directRecipient={recipient}
|
||||
directRecipientFields={directTemplateRecipient.Field}
|
||||
directRecipientFields={directTemplateRecipient.fields}
|
||||
template={template}
|
||||
onSubmit={onSignDirectTemplateSubmit}
|
||||
/>
|
||||
@ -1,11 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
@ -19,9 +18,7 @@ export const DirectTemplateAuthPageView = () => {
|
||||
try {
|
||||
setIsSigningOut(true);
|
||||
|
||||
await signOut({
|
||||
callbackUrl: '/signin',
|
||||
});
|
||||
await authClient.signOut();
|
||||
} catch {
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
@ -1,6 +1,8 @@
|
||||
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 { match } from 'ts-pattern';
|
||||
|
||||
@ -14,10 +16,8 @@ import {
|
||||
ZRadioFieldMeta,
|
||||
ZTextFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import type { TTemplate } from '@documenso/lib/types/template';
|
||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||
import type { Field, Recipient, Signature } from '@documenso/prisma/client';
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
import type { TemplateWithDetails } from '@documenso/prisma/types/template';
|
||||
import type {
|
||||
TRemovedSignedFieldWithTokenMutationSchema,
|
||||
TSignFieldWithTokenMutationSchema,
|
||||
@ -38,41 +38,41 @@ import { Label } from '@documenso/ui/primitives/label';
|
||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||
import { useStep } from '@documenso/ui/primitives/stepper';
|
||||
|
||||
import { CheckboxField } from '~/app/(signing)/sign/[token]/checkbox-field';
|
||||
import { DateField } from '~/app/(signing)/sign/[token]/date-field';
|
||||
import { DropdownField } from '~/app/(signing)/sign/[token]/dropdown-field';
|
||||
import { EmailField } from '~/app/(signing)/sign/[token]/email-field';
|
||||
import { InitialsField } from '~/app/(signing)/sign/[token]/initials-field';
|
||||
import { NameField } from '~/app/(signing)/sign/[token]/name-field';
|
||||
import { NumberField } from '~/app/(signing)/sign/[token]/number-field';
|
||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
||||
import { RadioField } from '~/app/(signing)/sign/[token]/radio-field';
|
||||
import { SignDialog } from '~/app/(signing)/sign/[token]/sign-dialog';
|
||||
import { SignatureField } from '~/app/(signing)/sign/[token]/signature-field';
|
||||
import { TextField } from '~/app/(signing)/sign/[token]/text-field';
|
||||
import { DocumentSigningCheckboxField } from '~/components/general/document-signing/document-signing-checkbox-field';
|
||||
import { DocumentSigningCompleteDialog } from '~/components/general/document-signing/document-signing-complete-dialog';
|
||||
import { DocumentSigningDateField } from '~/components/general/document-signing/document-signing-date-field';
|
||||
import { DocumentSigningDropdownField } from '~/components/general/document-signing/document-signing-dropdown-field';
|
||||
import { DocumentSigningEmailField } from '~/components/general/document-signing/document-signing-email-field';
|
||||
import { DocumentSigningInitialsField } from '~/components/general/document-signing/document-signing-initials-field';
|
||||
import { DocumentSigningNameField } from '~/components/general/document-signing/document-signing-name-field';
|
||||
import { DocumentSigningNumberField } from '~/components/general/document-signing/document-signing-number-field';
|
||||
import { useRequiredDocumentSigningContext } from '~/components/general/document-signing/document-signing-provider';
|
||||
import { DocumentSigningRadioField } from '~/components/general/document-signing/document-signing-radio-field';
|
||||
import { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-field';
|
||||
import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
|
||||
|
||||
export type SignDirectTemplateFormProps = {
|
||||
export type DirectTemplateSigningFormProps = {
|
||||
flowStep: DocumentFlowStep;
|
||||
directRecipient: Recipient;
|
||||
directRecipientFields: Field[];
|
||||
template: Omit<TemplateWithDetails, 'User'>;
|
||||
template: Omit<TTemplate, 'user'>;
|
||||
onSubmit: (_data: DirectTemplateLocalField[]) => Promise<void>;
|
||||
};
|
||||
|
||||
export type DirectTemplateLocalField = Field & {
|
||||
signedValue?: TSignFieldWithTokenMutationSchema;
|
||||
Signature?: Signature;
|
||||
signature?: Signature;
|
||||
};
|
||||
|
||||
export const SignDirectTemplateForm = ({
|
||||
export const DirectTemplateSigningForm = ({
|
||||
flowStep,
|
||||
directRecipient,
|
||||
directRecipientFields,
|
||||
template,
|
||||
onSubmit,
|
||||
}: SignDirectTemplateFormProps) => {
|
||||
}: DirectTemplateSigningFormProps) => {
|
||||
const { fullName, signature, signatureValid, setFullName, setSignature } =
|
||||
useRequiredSigningContext();
|
||||
useRequiredDocumentSigningContext();
|
||||
|
||||
const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(directRecipientFields);
|
||||
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||
@ -95,7 +95,7 @@ export const SignDirectTemplateForm = ({
|
||||
};
|
||||
|
||||
if (field.type === FieldType.SIGNATURE) {
|
||||
tempField.Signature = {
|
||||
tempField.signature = {
|
||||
id: 1,
|
||||
created: new Date(),
|
||||
recipientId: 1,
|
||||
@ -127,7 +127,7 @@ export const SignDirectTemplateForm = ({
|
||||
customText: '',
|
||||
inserted: false,
|
||||
signedValue: undefined,
|
||||
Signature: undefined,
|
||||
signature: undefined,
|
||||
};
|
||||
}),
|
||||
);
|
||||
@ -183,7 +183,7 @@ export const SignDirectTemplateForm = ({
|
||||
{localFields.map((field) =>
|
||||
match(field.type)
|
||||
.with(FieldType.SIGNATURE, () => (
|
||||
<SignatureField
|
||||
<DocumentSigningSignatureField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={directRecipient}
|
||||
@ -192,7 +192,7 @@ export const SignDirectTemplateForm = ({
|
||||
/>
|
||||
))
|
||||
.with(FieldType.INITIALS, () => (
|
||||
<InitialsField
|
||||
<DocumentSigningInitialsField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={directRecipient}
|
||||
@ -201,7 +201,7 @@ export const SignDirectTemplateForm = ({
|
||||
/>
|
||||
))
|
||||
.with(FieldType.NAME, () => (
|
||||
<NameField
|
||||
<DocumentSigningNameField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={directRecipient}
|
||||
@ -210,7 +210,7 @@ export const SignDirectTemplateForm = ({
|
||||
/>
|
||||
))
|
||||
.with(FieldType.DATE, () => (
|
||||
<DateField
|
||||
<DocumentSigningDateField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={directRecipient}
|
||||
@ -221,7 +221,7 @@ export const SignDirectTemplateForm = ({
|
||||
/>
|
||||
))
|
||||
.with(FieldType.EMAIL, () => (
|
||||
<EmailField
|
||||
<DocumentSigningEmailField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={directRecipient}
|
||||
@ -235,7 +235,7 @@ export const SignDirectTemplateForm = ({
|
||||
: null;
|
||||
|
||||
return (
|
||||
<TextField
|
||||
<DocumentSigningTextField
|
||||
key={field.id}
|
||||
field={{
|
||||
...field,
|
||||
@ -253,7 +253,7 @@ export const SignDirectTemplateForm = ({
|
||||
: null;
|
||||
|
||||
return (
|
||||
<NumberField
|
||||
<DocumentSigningNumberField
|
||||
key={field.id}
|
||||
field={{
|
||||
...field,
|
||||
@ -271,7 +271,7 @@ export const SignDirectTemplateForm = ({
|
||||
: null;
|
||||
|
||||
return (
|
||||
<DropdownField
|
||||
<DocumentSigningDropdownField
|
||||
key={field.id}
|
||||
field={{
|
||||
...field,
|
||||
@ -289,7 +289,7 @@ export const SignDirectTemplateForm = ({
|
||||
: null;
|
||||
|
||||
return (
|
||||
<RadioField
|
||||
<DocumentSigningRadioField
|
||||
key={field.id}
|
||||
field={{
|
||||
...field,
|
||||
@ -307,7 +307,7 @@ export const SignDirectTemplateForm = ({
|
||||
: null;
|
||||
|
||||
return (
|
||||
<CheckboxField
|
||||
<DocumentSigningCheckboxField
|
||||
key={field.id}
|
||||
field={{
|
||||
...field,
|
||||
@ -373,7 +373,7 @@ export const SignDirectTemplateForm = ({
|
||||
<Trans>Back</Trans>
|
||||
</Button>
|
||||
|
||||
<SignDialog
|
||||
<DocumentSigningCompleteDialog
|
||||
isSubmitting={isSubmitting}
|
||||
onSignatureComplete={handleSubmit}
|
||||
documentTitle={template.title}
|
||||
@ -1,13 +1,13 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
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 { z } from 'zod';
|
||||
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
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 { Button } from '@documenso/ui/primitives/button';
|
||||
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 { useRequiredDocumentAuthContext } from './document-auth-provider';
|
||||
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||
|
||||
export type DocumentActionAuth2FAProps = {
|
||||
export type DocumentSigningAuth2FAProps = {
|
||||
actionTarget?: 'FIELD' | 'DOCUMENT';
|
||||
actionVerb?: string;
|
||||
open: boolean;
|
||||
@ -42,15 +42,15 @@ const Z2FAAuthFormSchema = z.object({
|
||||
|
||||
type T2FAAuthFormSchema = z.infer<typeof Z2FAAuthFormSchema>;
|
||||
|
||||
export const DocumentActionAuth2FA = ({
|
||||
export const DocumentSigningAuth2FA = ({
|
||||
actionTarget = 'FIELD',
|
||||
actionVerb = 'sign',
|
||||
onReauthFormSubmit,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: DocumentActionAuth2FAProps) => {
|
||||
}: DocumentSigningAuth2FAProps) => {
|
||||
const { recipient, user, isCurrentlyAuthenticating, setIsCurrentlyAuthenticating } =
|
||||
useRequiredDocumentAuthContext();
|
||||
useRequiredDocumentSigningAuthContext();
|
||||
|
||||
const form = useForm<T2FAAuthFormSchema>({
|
||||
resolver: zodResolver(Z2FAAuthFormSchema),
|
||||
@ -109,17 +109,14 @@ export const DocumentActionAuth2FA = ({
|
||||
)}
|
||||
</p>
|
||||
|
||||
{user?.identityProvider === 'DOCUMENSO' && (
|
||||
<p className="mt-2">
|
||||
<Trans>
|
||||
By enabling 2FA, you will be required to enter a code from your authenticator app
|
||||
every time you sign in.
|
||||
</Trans>
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-2">
|
||||
<Trans>
|
||||
By enabling 2FA, you will be required to enter a code from your authenticator app
|
||||
every time you sign in using email password.
|
||||
</Trans>
|
||||
</p>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
||||
<Trans>Close</Trans>
|
||||
@ -1,31 +1,32 @@
|
||||
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 { signOut } from 'next-auth/react';
|
||||
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
import { authClient } from '@documenso/auth/client';
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
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';
|
||||
actionVerb?: string;
|
||||
onOpenChange: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export const DocumentActionAuthAccount = ({
|
||||
export const DocumentSigningAuthAccount = ({
|
||||
actionTarget = 'FIELD',
|
||||
actionVerb = 'sign',
|
||||
onOpenChange,
|
||||
}: DocumentActionAuthAccountProps) => {
|
||||
const { recipient } = useRequiredDocumentAuthContext();
|
||||
}: DocumentSigningAuthAccountProps) => {
|
||||
const { recipient } = useRequiredDocumentSigningAuthContext();
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useLingui();
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const [isSigningOut, setIsSigningOut] = useState(false);
|
||||
|
||||
@ -33,15 +34,18 @@ export const DocumentActionAuthAccount = ({
|
||||
try {
|
||||
setIsSigningOut(true);
|
||||
|
||||
await signOut({
|
||||
redirect: false,
|
||||
await authClient.signOut({
|
||||
redirectPath: `/signin#email=${email}`,
|
||||
});
|
||||
|
||||
router.push(`/signin#email=${email}`);
|
||||
} catch {
|
||||
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 {
|
||||
@ -6,7 +7,6 @@ import {
|
||||
type TRecipientActionAuth,
|
||||
type TRecipientActionAuthTypes,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import type { FieldType } from '@documenso/prisma/client';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@ -15,12 +15,12 @@ import {
|
||||
DialogTitle,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
|
||||
import { DocumentActionAuth2FA } from './document-action-auth-2fa';
|
||||
import { DocumentActionAuthAccount } from './document-action-auth-account';
|
||||
import { DocumentActionAuthPasskey } from './document-action-auth-passkey';
|
||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
||||
import { DocumentSigningAuth2FA } from './document-signing-auth-2fa';
|
||||
import { DocumentSigningAuthAccount } from './document-signing-auth-account';
|
||||
import { DocumentSigningAuthPasskey } from './document-signing-auth-passkey';
|
||||
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||
|
||||
export type DocumentActionAuthDialogProps = {
|
||||
export type DocumentSigningAuthDialogProps = {
|
||||
title?: string;
|
||||
documentAuthType: TRecipientActionAuthTypes;
|
||||
description?: string;
|
||||
@ -34,15 +34,15 @@ export type DocumentActionAuthDialogProps = {
|
||||
onReauthFormSubmit: (values?: TRecipientActionAuth) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const DocumentActionAuthDialog = ({
|
||||
export const DocumentSigningAuthDialog = ({
|
||||
title,
|
||||
description,
|
||||
documentAuthType,
|
||||
open,
|
||||
onOpenChange,
|
||||
onReauthFormSubmit,
|
||||
}: DocumentActionAuthDialogProps) => {
|
||||
const { recipient, user, isCurrentlyAuthenticating } = useRequiredDocumentAuthContext();
|
||||
}: DocumentSigningAuthDialogProps) => {
|
||||
const { recipient, user, isCurrentlyAuthenticating } = useRequiredDocumentSigningAuthContext();
|
||||
|
||||
const handleOnOpenChange = (value: boolean) => {
|
||||
if (isCurrentlyAuthenticating) {
|
||||
@ -67,17 +67,17 @@ export const DocumentActionAuthDialog = ({
|
||||
.with(
|
||||
{ documentAuthType: DocumentAuth.ACCOUNT },
|
||||
{ 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 }, () => (
|
||||
<DocumentActionAuthPasskey
|
||||
<DocumentSigningAuthPasskey
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
onReauthFormSubmit={onReauthFormSubmit}
|
||||
/>
|
||||
))
|
||||
.with({ documentAuthType: DocumentAuth.TWO_FACTOR_AUTH }, () => (
|
||||
<DocumentActionAuth2FA
|
||||
<DocumentSigningAuth2FA
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
onReauthFormSubmit={onReauthFormSubmit}
|
||||
@ -1,38 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type SigningAuthPageViewProps = {
|
||||
export type DocumentSigningAuthPageViewProps = {
|
||||
email: string;
|
||||
emailHasAccount?: boolean;
|
||||
};
|
||||
|
||||
export const SigningAuthPageView = ({ email, emailHasAccount }: SigningAuthPageViewProps) => {
|
||||
export const DocumentSigningAuthPageView = ({
|
||||
email,
|
||||
emailHasAccount,
|
||||
}: DocumentSigningAuthPageViewProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const [isSigningOut, setIsSigningOut] = useState(false);
|
||||
|
||||
const handleChangeAccount = async (email: string) => {
|
||||
try {
|
||||
setIsSigningOut(true);
|
||||
|
||||
await signOut({
|
||||
redirect: false,
|
||||
await authClient.signOut({
|
||||
redirectPath: emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`,
|
||||
});
|
||||
|
||||
router.push(emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`);
|
||||
} catch {
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
@ -1,8 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@ -10,7 +12,6 @@ import { z } from 'zod';
|
||||
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { DocumentAuth, type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -31,11 +32,11 @@ import {
|
||||
SelectValue,
|
||||
} 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';
|
||||
actionVerb?: string;
|
||||
open: boolean;
|
||||
@ -49,13 +50,13 @@ const ZPasskeyAuthFormSchema = z.object({
|
||||
|
||||
type TPasskeyAuthFormSchema = z.infer<typeof ZPasskeyAuthFormSchema>;
|
||||
|
||||
export const DocumentActionAuthPasskey = ({
|
||||
export const DocumentSigningAuthPasskey = ({
|
||||
actionTarget = 'FIELD',
|
||||
actionVerb = 'sign',
|
||||
onReauthFormSubmit,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: DocumentActionAuthPasskeyProps) => {
|
||||
}: DocumentSigningAuthPasskeyProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const {
|
||||
@ -66,7 +67,7 @@ export const DocumentActionAuthPasskey = ({
|
||||
isCurrentlyAuthenticating,
|
||||
setIsCurrentlyAuthenticating,
|
||||
refetchPasskeys,
|
||||
} = useRequiredDocumentAuthContext();
|
||||
} = useRequiredDocumentSigningAuthContext();
|
||||
|
||||
const form = useForm<TPasskeyAuthFormSchema>({
|
||||
resolver: zodResolver(ZPasskeyAuthFormSchema),
|
||||
@ -189,7 +190,7 @@ export const DocumentActionAuthPasskey = ({
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<CreatePasskeyDialog
|
||||
<PasskeyCreateDialog
|
||||
onSuccess={async () => refetchPasskeys()}
|
||||
trigger={
|
||||
<Button>
|
||||
@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
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 type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
||||
import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth';
|
||||
import type {
|
||||
TDocumentAuthOptions,
|
||||
@ -13,17 +13,10 @@ import type {
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { DocumentAuth } from '@documenso/lib/types/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 type { DocumentActionAuthDialogProps } from './document-action-auth-dialog';
|
||||
import { DocumentActionAuthDialog } from './document-action-auth-dialog';
|
||||
import type { DocumentSigningAuthDialogProps } from './document-signing-auth-dialog';
|
||||
import { DocumentSigningAuthDialog } from './document-signing-auth-dialog';
|
||||
|
||||
type PasskeyData = {
|
||||
passkeys: Omit<Passkey, 'credentialId' | 'credentialPublicKey'>[];
|
||||
@ -32,7 +25,7 @@ type PasskeyData = {
|
||||
isError: boolean;
|
||||
};
|
||||
|
||||
export type DocumentAuthContextValue = {
|
||||
export type DocumentSigningAuthContextValue = {
|
||||
executeActionAuthProcedure: (_value: ExecuteActionAuthProcedureOptions) => Promise<void>;
|
||||
documentAuthOptions: Document['authOptions'];
|
||||
documentAuthOption: TDocumentAuthOptions;
|
||||
@ -48,39 +41,39 @@ export type DocumentAuthContextValue = {
|
||||
passkeyData: PasskeyData;
|
||||
preferredPasskeyId: string | null;
|
||||
setPreferredPasskeyId: (_value: string | null) => void;
|
||||
user?: User | null;
|
||||
user?: SessionUser | null;
|
||||
refetchPasskeys: () => Promise<void>;
|
||||
};
|
||||
|
||||
const DocumentAuthContext = createContext<DocumentAuthContextValue | null>(null);
|
||||
const DocumentSigningAuthContext = createContext<DocumentSigningAuthContextValue | null>(null);
|
||||
|
||||
export const useDocumentAuthContext = () => {
|
||||
return useContext(DocumentAuthContext);
|
||||
export const useDocumentSigningAuthContext = () => {
|
||||
return useContext(DocumentSigningAuthContext);
|
||||
};
|
||||
|
||||
export const useRequiredDocumentAuthContext = () => {
|
||||
const context = useDocumentAuthContext();
|
||||
export const useRequiredDocumentSigningAuthContext = () => {
|
||||
const context = useDocumentSigningAuthContext();
|
||||
|
||||
if (!context) {
|
||||
throw new Error('Document auth context is required');
|
||||
throw new Error('Document signing auth context is required');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export interface DocumentAuthProviderProps {
|
||||
export interface DocumentSigningAuthProviderProps {
|
||||
documentAuthOptions: Document['authOptions'];
|
||||
recipient: Recipient;
|
||||
user?: User | null;
|
||||
user?: SessionUser | null;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const DocumentAuthProvider = ({
|
||||
export const DocumentSigningAuthProvider = ({
|
||||
documentAuthOptions: initialDocumentAuthOptions,
|
||||
recipient: initialRecipient,
|
||||
user,
|
||||
children,
|
||||
}: DocumentAuthProviderProps) => {
|
||||
}: DocumentSigningAuthProviderProps) => {
|
||||
const [documentAuthOptions, setDocumentAuthOptions] = useState(initialDocumentAuthOptions);
|
||||
const [recipient, setRecipient] = useState(initialRecipient);
|
||||
|
||||
@ -106,7 +99,7 @@ export const DocumentAuthProvider = ({
|
||||
perPage: MAXIMUM_PASSKEYS,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
placeholderData: (previousData) => previousData,
|
||||
enabled: derivedRecipientActionAuth === DocumentAuth.PASSKEY,
|
||||
},
|
||||
);
|
||||
@ -186,7 +179,7 @@ export const DocumentAuthProvider = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<DocumentAuthContext.Provider
|
||||
<DocumentSigningAuthContext.Provider
|
||||
value={{
|
||||
user,
|
||||
documentAuthOptions,
|
||||
@ -210,7 +203,7 @@ export const DocumentAuthProvider = ({
|
||||
{children}
|
||||
|
||||
{documentAuthDialogPayload && derivedRecipientActionAuth && (
|
||||
<DocumentActionAuthDialog
|
||||
<DocumentSigningAuthDialog
|
||||
open={true}
|
||||
onOpenChange={() => setDocumentAuthDialogPayload(null)}
|
||||
onReauthFormSubmit={documentAuthDialogPayload.onReauthFormSubmit}
|
||||
@ -218,13 +211,13 @@ export const DocumentAuthProvider = ({
|
||||
documentAuthType={derivedRecipientActionAuth}
|
||||
/>
|
||||
)}
|
||||
</DocumentAuthContext.Provider>
|
||||
</DocumentSigningAuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
type ExecuteActionAuthProcedureOptions = Omit<
|
||||
DocumentActionAuthDialogProps,
|
||||
DocumentSigningAuthDialogProps,
|
||||
'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 { useRouter } from 'next/navigation';
|
||||
|
||||
import { Plural, Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
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 { useRevalidator } from 'react-router';
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
|
||||
import { DocumentAuth } from '@documenso/lib/types/document-auth';
|
||||
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 { Button } from '@documenso/ui/primitives/button';
|
||||
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 { 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 { useRequiredSigningContext } from './provider';
|
||||
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||
|
||||
const AUTO_SIGNABLE_FIELD_TYPES: string[] = [
|
||||
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.
|
||||
const AUTO_SIGN_THRESHOLD = 5;
|
||||
|
||||
export type AutoSignProps = {
|
||||
export type DocumentSigningAutoSignProps = {
|
||||
recipient: Pick<Recipient, 'id' | 'token'>;
|
||||
fields: Field[];
|
||||
};
|
||||
|
||||
export const AutoSign = ({ recipient, fields }: AutoSignProps) => {
|
||||
export const DocumentSigningAutoSign = ({ recipient, fields }: DocumentSigningAutoSignProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { revalidate } = useRevalidator();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { email, fullName } = useRequiredSigningContext();
|
||||
const { derivedRecipientActionAuth } = useRequiredDocumentAuthContext();
|
||||
const { email, fullName } = useRequiredDocumentSigningContext();
|
||||
const { derivedRecipientActionAuth } = useRequiredDocumentSigningAuthContext();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const form = useForm();
|
||||
|
||||
@ -158,11 +154,7 @@ export const AutoSign = ({ recipient, fields }: AutoSignProps) => {
|
||||
});
|
||||
}
|
||||
|
||||
startTransition(() => {
|
||||
router.refresh();
|
||||
|
||||
setOpen(false);
|
||||
});
|
||||
await revalidate();
|
||||
};
|
||||
|
||||
unsafe_useEffectOnce(() => {
|
||||
@ -205,7 +197,7 @@ export const AutoSign = ({ recipient, fields }: AutoSignProps) => {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<SigningDisclosure className="mt-4" />
|
||||
<DocumentSigningDisclosure className="mt-4" />
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
@ -223,7 +215,7 @@ export const AutoSign = ({ recipient, fields }: AutoSignProps) => {
|
||||
<Button
|
||||
type="submit"
|
||||
className="min-w-[6rem]"
|
||||
loading={form.formState.isSubmitting || isPending}
|
||||
loading={form.formState.isSubmitting}
|
||||
disabled={!autoSignableFields.length}
|
||||
>
|
||||
<Trans>Sign</Trans>
|
||||
@ -1,19 +1,16 @@
|
||||
'use client';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { useEffect, useMemo, useState, useTransition } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import type { Recipient } from '@prisma/client';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { useRevalidator } from 'react-router';
|
||||
|
||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import { ZCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
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 { trpc } from '@documenso/trpc/react';
|
||||
import type {
|
||||
@ -26,28 +23,27 @@ import { checkboxValidationSigns } from '@documenso/ui/primitives/document-flow/
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
||||
import { SigningFieldContainer } from './signing-field-container';
|
||||
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||
|
||||
export type CheckboxFieldProps = {
|
||||
export type DocumentSigningCheckboxFieldProps = {
|
||||
field: FieldWithSignatureAndFieldMeta;
|
||||
recipient: Recipient;
|
||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const CheckboxField = ({
|
||||
export const DocumentSigningCheckboxField = ({
|
||||
field,
|
||||
recipient,
|
||||
onSignField,
|
||||
onUnsignField,
|
||||
}: CheckboxFieldProps) => {
|
||||
}: DocumentSigningCheckboxFieldProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { revalidate } = useRevalidator();
|
||||
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
|
||||
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
||||
|
||||
const parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
||||
|
||||
@ -81,15 +77,15 @@ export const CheckboxField = ({
|
||||
);
|
||||
}, [checkedValues, validationSign, checkboxValidationLength]);
|
||||
|
||||
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } =
|
||||
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
||||
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||
|
||||
const {
|
||||
mutateAsync: removeSignedFieldWithToken,
|
||||
isLoading: isRemoveSignedFieldWithTokenLoading,
|
||||
isPending: isRemoveSignedFieldWithTokenLoading,
|
||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||
|
||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
|
||||
const shouldAutoSignField =
|
||||
(!field.inserted && checkedValues.length > 0 && isLengthConditionMet) ||
|
||||
(!field.inserted && isReadOnly && isLengthConditionMet);
|
||||
@ -110,7 +106,7 @@ export const CheckboxField = ({
|
||||
await signFieldWithToken(payload);
|
||||
}
|
||||
|
||||
startTransition(() => router.refresh());
|
||||
await revalidate();
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
@ -145,7 +141,7 @@ export const CheckboxField = ({
|
||||
setCheckedValues([]);
|
||||
}
|
||||
|
||||
startTransition(() => router.refresh());
|
||||
await revalidate();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@ -217,7 +213,7 @@ export const CheckboxField = ({
|
||||
});
|
||||
} finally {
|
||||
setCheckedValues(updatedValues);
|
||||
startTransition(() => router.refresh());
|
||||
await revalidate();
|
||||
}
|
||||
};
|
||||
|
||||
@ -236,7 +232,12 @@ export const CheckboxField = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Checkbox">
|
||||
<DocumentSigningFieldContainer
|
||||
field={field}
|
||||
onSign={onSign}
|
||||
onRemove={onRemove}
|
||||
type="Checkbox"
|
||||
>
|
||||
{isLoading && (
|
||||
<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" />
|
||||
@ -294,6 +295,6 @@ export const CheckboxField = ({
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</SigningFieldContainer>
|
||||
</DocumentSigningFieldContainer>
|
||||
);
|
||||
};
|
||||
@ -1,10 +1,10 @@
|
||||
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 type { Field } from '@documenso/prisma/client';
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
@ -14,9 +14,9 @@ import {
|
||||
DialogTrigger,
|
||||
} 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;
|
||||
documentTitle: string;
|
||||
fields: Field[];
|
||||
@ -26,7 +26,7 @@ export type SignDialogProps = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const SignDialog = ({
|
||||
export const DocumentSigningCompleteDialog = ({
|
||||
isSubmitting,
|
||||
documentTitle,
|
||||
fields,
|
||||
@ -34,7 +34,7 @@ export const SignDialog = ({
|
||||
onSignatureComplete,
|
||||
role,
|
||||
disabled = false,
|
||||
}: SignDialogProps) => {
|
||||
}: DocumentSigningCompleteDialogProps) => {
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
|
||||
const isComplete = useMemo(() => !fieldsContainUnsignedRequiredField(fields), [fields]);
|
||||
@ -116,7 +116,7 @@ export const SignDialog = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SigningDisclosure className="mt-4" />
|
||||
<DocumentSigningDisclosure className="mt-4" />
|
||||
|
||||
<DialogFooter>
|
||||
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
||||
@ -1,12 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useTransition } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { Recipient } from '@prisma/client';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { useRevalidator } from 'react-router';
|
||||
|
||||
import {
|
||||
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 { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
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 { trpc } from '@documenso/trpc/react';
|
||||
import type {
|
||||
@ -25,9 +21,9 @@ import type {
|
||||
} from '@documenso/trpc/server/field-router/schema';
|
||||
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;
|
||||
recipient: Recipient;
|
||||
dateFormat?: string | null;
|
||||
@ -36,37 +32,34 @@ export type DateFieldProps = {
|
||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const DateField = ({
|
||||
export const DocumentSigningDateField = ({
|
||||
field,
|
||||
recipient,
|
||||
dateFormat = DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||
timezone = DEFAULT_DOCUMENT_TIME_ZONE,
|
||||
onSignField,
|
||||
onUnsignField,
|
||||
}: DateFieldProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
}: DocumentSigningDateFieldProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { revalidate } = useRevalidator();
|
||||
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } =
|
||||
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
||||
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||
|
||||
const {
|
||||
mutateAsync: removeSignedFieldWithToken,
|
||||
isLoading: isRemoveSignedFieldWithTokenLoading,
|
||||
isPending: isRemoveSignedFieldWithTokenLoading,
|
||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||
|
||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
|
||||
|
||||
const localDateString = convertToLocalSystemFormat(field.customText, dateFormat, timezone);
|
||||
|
||||
const isDifferentTime = field.inserted && localDateString !== field.customText;
|
||||
|
||||
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) => {
|
||||
@ -85,7 +78,7 @@ export const DateField = ({
|
||||
|
||||
await signFieldWithToken(payload);
|
||||
|
||||
startTransition(() => router.refresh());
|
||||
await revalidate();
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
@ -117,7 +110,7 @@ export const DateField = ({
|
||||
|
||||
await removeSignedFieldWithToken(payload);
|
||||
|
||||
startTransition(() => router.refresh());
|
||||
await revalidate();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@ -130,7 +123,7 @@ export const DateField = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<SigningFieldContainer
|
||||
<DocumentSigningFieldContainer
|
||||
field={field}
|
||||
onSign={onSign}
|
||||
onRemove={onRemove}
|
||||
@ -154,6 +147,6 @@ export const DateField = ({
|
||||
{localDateString}
|
||||
</p>
|
||||
)}
|
||||
</SigningFieldContainer>
|
||||
</DocumentSigningFieldContainer>
|
||||
);
|
||||
};
|
||||
@ -1,14 +1,16 @@
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
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 (
|
||||
<p className={cn('text-muted-foreground text-xs', className)} {...props}>
|
||||
<Trans>
|
||||
@ -22,7 +24,7 @@ export const SigningDisclosure = ({ className, ...props }: SigningDisclosureProp
|
||||
Read the full{' '}
|
||||
<Link
|
||||
className="text-documenso-700 underline"
|
||||
href="/articles/signature-disclosure"
|
||||
to="/articles/signature-disclosure"
|
||||
target="_blank"
|
||||
>
|
||||
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