mirror of
https://github.com/documenso/documenso.git
synced 2025-11-09 20:12:31 +10:00
feat: migrate nextjs to rr7
This commit is contained in:
@ -5,6 +5,7 @@ module.exports = {
|
|||||||
rules: {
|
rules: {
|
||||||
'@next/next/no-img-element': 'off',
|
'@next/next/no-img-element': 'off',
|
||||||
'no-unreachable': 'error',
|
'no-unreachable': 'error',
|
||||||
|
'react-hooks/exhaustive-deps': 'off',
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
next: {
|
next: {
|
||||||
|
|||||||
28
apps/remix/.bin/build.sh
Executable file
28
apps/remix/.bin/build.sh
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Exit on error.
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
start_time=$(date +%s)
|
||||||
|
|
||||||
|
echo "[Build]: Extracting and compiling translations"
|
||||||
|
npm run translate --prefix ../../
|
||||||
|
|
||||||
|
echo "[Build]: Building app"
|
||||||
|
npm run build:app
|
||||||
|
|
||||||
|
echo "[Build]: Building server"
|
||||||
|
npm run build:server
|
||||||
|
|
||||||
|
# Copy over the entry point for the server.
|
||||||
|
cp server/main.js build/server/main.js
|
||||||
|
|
||||||
|
# Copy over all web.js translations
|
||||||
|
cp -r ../../packages/lib/translations build/server/hono/packages/lib/translations
|
||||||
|
|
||||||
|
# Time taken
|
||||||
|
end_time=$(date +%s)
|
||||||
|
|
||||||
|
echo "[Build]: Done in $((end_time - start_time)) seconds"
|
||||||
4
apps/remix/.dockerignore
Normal file
4
apps/remix/.dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.react-router
|
||||||
|
build
|
||||||
|
node_modules
|
||||||
|
README.md
|
||||||
9
apps/remix/.gitignore
vendored
Normal file
9
apps/remix/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.DS_Store
|
||||||
|
/node_modules/
|
||||||
|
|
||||||
|
# React Router
|
||||||
|
/.react-router/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.*.timestamp*
|
||||||
22
apps/remix/Dockerfile
Normal file
22
apps/remix/Dockerfile
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
FROM node:20-alpine AS development-dependencies-env
|
||||||
|
COPY . /app
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
FROM node:20-alpine AS production-dependencies-env
|
||||||
|
COPY ./package.json package-lock.json /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
|
FROM node:20-alpine AS build-env
|
||||||
|
COPY . /app/
|
||||||
|
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:20-alpine
|
||||||
|
COPY ./package.json package-lock.json /app/
|
||||||
|
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
COPY --from=build-env /app/build /app/build
|
||||||
|
WORKDIR /app
|
||||||
|
CMD ["npm", "run", "start"]
|
||||||
25
apps/remix/Dockerfile.bun
Normal file
25
apps/remix/Dockerfile.bun
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
FROM oven/bun:1 AS dependencies-env
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
FROM dependencies-env AS development-dependencies-env
|
||||||
|
COPY ./package.json bun.lockb /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN bun i --frozen-lockfile
|
||||||
|
|
||||||
|
FROM dependencies-env AS production-dependencies-env
|
||||||
|
COPY ./package.json bun.lockb /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN bun i --production
|
||||||
|
|
||||||
|
FROM dependencies-env AS build-env
|
||||||
|
COPY ./package.json bun.lockb /app/
|
||||||
|
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
WORKDIR /app
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
FROM dependencies-env
|
||||||
|
COPY ./package.json bun.lockb /app/
|
||||||
|
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
COPY --from=build-env /app/build /app/build
|
||||||
|
WORKDIR /app
|
||||||
|
CMD ["bun", "run", "start"]
|
||||||
26
apps/remix/Dockerfile.pnpm
Normal file
26
apps/remix/Dockerfile.pnpm
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
FROM node:20-alpine AS dependencies-env
|
||||||
|
RUN npm i -g pnpm
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
FROM dependencies-env AS development-dependencies-env
|
||||||
|
COPY ./package.json pnpm-lock.yaml /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN pnpm i --frozen-lockfile
|
||||||
|
|
||||||
|
FROM dependencies-env AS production-dependencies-env
|
||||||
|
COPY ./package.json pnpm-lock.yaml /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN pnpm i --prod --frozen-lockfile
|
||||||
|
|
||||||
|
FROM dependencies-env AS build-env
|
||||||
|
COPY ./package.json pnpm-lock.yaml /app/
|
||||||
|
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
WORKDIR /app
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
FROM dependencies-env
|
||||||
|
COPY ./package.json pnpm-lock.yaml /app/
|
||||||
|
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
COPY --from=build-env /app/build /app/build
|
||||||
|
WORKDIR /app
|
||||||
|
CMD ["pnpm", "start"]
|
||||||
100
apps/remix/README.md
Normal file
100
apps/remix/README.md
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# Welcome to React Router!
|
||||||
|
|
||||||
|
A modern, production-ready template for building full-stack React applications using React Router.
|
||||||
|
|
||||||
|
[](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🚀 Server-side rendering
|
||||||
|
- ⚡️ Hot Module Replacement (HMR)
|
||||||
|
- 📦 Asset bundling and optimization
|
||||||
|
- 🔄 Data loading and mutations
|
||||||
|
- 🔒 TypeScript by default
|
||||||
|
- 🎉 TailwindCSS for styling
|
||||||
|
- 📖 [React Router docs](https://reactrouter.com/)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
Install the dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
Start the development server with HMR:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Your application will be available at `http://localhost:5173`.
|
||||||
|
|
||||||
|
## Building for Production
|
||||||
|
|
||||||
|
Create a production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Docker Deployment
|
||||||
|
|
||||||
|
This template includes three Dockerfiles optimized for different package managers:
|
||||||
|
|
||||||
|
- `Dockerfile` - for npm
|
||||||
|
- `Dockerfile.pnpm` - for pnpm
|
||||||
|
- `Dockerfile.bun` - for bun
|
||||||
|
|
||||||
|
To build and run using Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For npm
|
||||||
|
docker build -t my-app .
|
||||||
|
|
||||||
|
# For pnpm
|
||||||
|
docker build -f Dockerfile.pnpm -t my-app .
|
||||||
|
|
||||||
|
# For bun
|
||||||
|
docker build -f Dockerfile.bun -t my-app .
|
||||||
|
|
||||||
|
# Run the container
|
||||||
|
docker run -p 3000:3000 my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
The containerized application can be deployed to any platform that supports Docker, including:
|
||||||
|
|
||||||
|
- AWS ECS
|
||||||
|
- Google Cloud Run
|
||||||
|
- Azure Container Apps
|
||||||
|
- Digital Ocean App Platform
|
||||||
|
- Fly.io
|
||||||
|
- Railway
|
||||||
|
|
||||||
|
### DIY Deployment
|
||||||
|
|
||||||
|
If you're familiar with deploying Node applications, the built-in app server is production-ready.
|
||||||
|
|
||||||
|
Make sure to deploy the output of `npm run build`
|
||||||
|
|
||||||
|
```
|
||||||
|
├── package.json
|
||||||
|
├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
|
||||||
|
├── build/
|
||||||
|
│ ├── client/ # Static assets
|
||||||
|
│ └── server/ # Server-side code
|
||||||
|
```
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Built with ❤️ using React Router.
|
||||||
24
apps/remix/app/app.css
Normal file
24
apps/remix/app/app.css
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
@import '@documenso/ui/styles/theme.css';
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src: url('/public/fonts/inter-regular.ttf') format('ttf');
|
||||||
|
/* font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap; */
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Caveat';
|
||||||
|
src: url('/public/fonts/caveat.ttf') format('ttf');
|
||||||
|
/* font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap; */
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--font-sans: 'Inter';
|
||||||
|
--font-signature: 'Caveat';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +1,11 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { signOut } from 'next-auth/react';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
|
||||||
import type { User } from '@documenso/prisma/client';
|
import { authClient } from '@documenso/auth/client';
|
||||||
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -23,12 +22,13 @@ import { Input } from '@documenso/ui/primitives/input';
|
|||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DeleteAccountDialogProps = {
|
export type AccountDeleteDialogProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
user: User;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProps) => {
|
export const AccountDeleteDialog = ({ className }: AccountDeleteDialogProps) => {
|
||||||
|
const { user } = useSession();
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProp
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await signOut({ callbackUrl: '/' });
|
return await authClient.signOut();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
@ -118,7 +118,7 @@ export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProp
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{!hasTwoFactorAuthentication && (
|
{!hasTwoFactorAuthentication && (
|
||||||
<div className="mt-4">
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
<Trans>
|
<Trans>
|
||||||
Please type{' '}
|
Please type{' '}
|
||||||
@ -1,13 +1,11 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Document } from '@prisma/client';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
import type { Document } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -23,15 +21,15 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type SuperDeleteDocumentDialogProps = {
|
export type AdminDocumentDeleteDialogProps = {
|
||||||
document: Document;
|
document: Document;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialogProps) => {
|
export const AdminDocumentDeleteDialog = ({ document }: AdminDocumentDeleteDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [reason, setReason] = useState('');
|
const [reason, setReason] = useState('');
|
||||||
|
|
||||||
@ -52,7 +50,7 @@ export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialo
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push('/admin/documents');
|
await navigate('/admin/documents');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
@ -1,15 +1,13 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { User } from '@prisma/client';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { User } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -25,17 +23,15 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DeleteUserDialogProps = {
|
export type AdminUserDeleteDialogProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
user: User;
|
user: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteUserDialog = ({ className, user }: DeleteUserDialogProps) => {
|
export const AdminUserDeleteDialog = ({ className, user }: AdminUserDeleteDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const navigate = useNavigate();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
|
||||||
const { mutateAsync: deleteUser, isPending: isDeletingUser } =
|
const { mutateAsync: deleteUser, isPending: isDeletingUser } =
|
||||||
@ -47,13 +43,13 @@ export const DeleteUserDialog = ({ className, user }: DeleteUserDialogProps) =>
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await navigate('/admin/users');
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Account deleted`),
|
title: _(msg`Account deleted`),
|
||||||
description: _(msg`The account has been deleted successfully.`),
|
description: _(msg`The account has been deleted successfully.`),
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push('/admin/users');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -1,13 +1,12 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { User } from '@prisma/client';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { User } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -23,12 +22,15 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DisableUserDialogProps = {
|
export type AdminUserDisableDialogProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
userToDisable: User;
|
userToDisable: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DisableUserDialog = ({ className, userToDisable }: DisableUserDialogProps) => {
|
export const AdminUserDisableDialog = ({
|
||||||
|
className,
|
||||||
|
userToDisable,
|
||||||
|
}: AdminUserDisableDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@ -1,13 +1,12 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { User } from '@prisma/client';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { User } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -23,12 +22,12 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type EnableUserDialogProps = {
|
export type AdminUserEnableDialogProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
userToEnable: User;
|
userToEnable: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EnableUserDialog = ({ className, userToEnable }: EnableUserDialogProps) => {
|
export const AdminUserEnableDialog = ({ className, userToEnable }: AdminUserEnableDialogProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
@ -1,13 +1,12 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { DocumentStatus } from '@prisma/client';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -22,26 +21,26 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
type DeleteDocumentDialogProps = {
|
type DocumentDeleteDialogProps = {
|
||||||
id: number;
|
id: number;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
|
onDelete?: () => Promise<void> | void;
|
||||||
status: DocumentStatus;
|
status: DocumentStatus;
|
||||||
documentTitle: string;
|
documentTitle: string;
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
canManageDocument: boolean;
|
canManageDocument: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteDocumentDialog = ({
|
export const DocumentDeleteDialog = ({
|
||||||
id,
|
id,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
|
onDelete,
|
||||||
status,
|
status,
|
||||||
documentTitle,
|
documentTitle,
|
||||||
canManageDocument,
|
canManageDocument,
|
||||||
}: DeleteDocumentDialogProps) => {
|
}: DocumentDeleteDialogProps) => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { refreshLimits } = useLimits();
|
const { refreshLimits } = useLimits();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -52,8 +51,7 @@ export const DeleteDocumentDialog = ({
|
|||||||
const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
|
const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
|
||||||
|
|
||||||
const { mutateAsync: deleteDocument, isPending } = trpcReact.document.deleteDocument.useMutation({
|
const { mutateAsync: deleteDocument, isPending } = trpcReact.document.deleteDocument.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: async () => {
|
||||||
router.refresh();
|
|
||||||
void refreshLimits();
|
void refreshLimits();
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@ -62,8 +60,18 @@ export const DeleteDocumentDialog = ({
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await onDelete?.();
|
||||||
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
},
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast({
|
||||||
|
title: _(msg`Something went wrong`),
|
||||||
|
description: _(msg`This document could not be deleted at this time. Please try again.`),
|
||||||
|
variant: 'destructive',
|
||||||
|
duration: 7500,
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -73,19 +81,6 @@ export const DeleteDocumentDialog = ({
|
|||||||
}
|
}
|
||||||
}, [open, status]);
|
}, [open, status]);
|
||||||
|
|
||||||
const onDelete = async () => {
|
|
||||||
try {
|
|
||||||
await deleteDocument({ documentId: id });
|
|
||||||
} catch {
|
|
||||||
toast({
|
|
||||||
title: _(msg`Something went wrong`),
|
|
||||||
description: _(msg`This document could not be deleted at this time. Please try again.`),
|
|
||||||
variant: 'destructive',
|
|
||||||
duration: 7500,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setInputValue(event.target.value);
|
setInputValue(event.target.value);
|
||||||
setIsDeleteEnabled(event.target.value === _(deleteMessage));
|
setIsDeleteEnabled(event.target.value === _(deleteMessage));
|
||||||
@ -194,7 +189,7 @@ export const DeleteDocumentDialog = ({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
loading={isPending}
|
loading={isPending}
|
||||||
onClick={onDelete}
|
onClick={() => void deleteDocument({ documentId: id })}
|
||||||
disabled={!isDeleteEnabled && canManageDocument}
|
disabled={!isDeleteEnabled && canManageDocument}
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
>
|
>
|
||||||
@ -1,10 +1,9 @@
|
|||||||
import { useRouter } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import type { Team } from '@documenso/prisma/client';
|
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -17,27 +16,34 @@ import {
|
|||||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
type DuplicateDocumentDialogProps = {
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
|
type DocumentDuplicateDialogProps = {
|
||||||
id: number;
|
id: number;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
team?: Pick<Team, 'id' | 'url'>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DuplicateDocumentDialog = ({
|
export const DocumentDuplicateDialog = ({
|
||||||
id,
|
id,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
team,
|
}: DocumentDuplicateDialogProps) => {
|
||||||
}: DuplicateDocumentDialogProps) => {
|
const navigate = useNavigate();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({
|
const team = useOptionalCurrentTeam();
|
||||||
documentId: id,
|
|
||||||
});
|
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery(
|
||||||
|
{
|
||||||
|
documentId: id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: open === true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const documentData = document?.documentData
|
const documentData = document?.documentData
|
||||||
? {
|
? {
|
||||||
@ -50,15 +56,14 @@ export const DuplicateDocumentDialog = ({
|
|||||||
|
|
||||||
const { mutateAsync: duplicateDocument, isPending: isDuplicateLoading } =
|
const { mutateAsync: duplicateDocument, isPending: isDuplicateLoading } =
|
||||||
trpcReact.document.duplicateDocument.useMutation({
|
trpcReact.document.duplicateDocument.useMutation({
|
||||||
onSuccess: ({ documentId }) => {
|
onSuccess: async ({ documentId }) => {
|
||||||
router.push(`${documentsPath}/${documentId}/edit`);
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Document Duplicated`),
|
title: _(msg`Document Duplicated`),
|
||||||
description: _(msg`Your document has been successfully duplicated.`),
|
description: _(msg`Your document has been successfully duplicated.`),
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await navigate(`${documentsPath}/${documentId}/edit`);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -1,11 +1,10 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -26,30 +25,28 @@ import {
|
|||||||
} from '@documenso/ui/primitives/select';
|
} from '@documenso/ui/primitives/select';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
type MoveDocumentDialogProps = {
|
type DocumentMoveDialogProps = {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocumentDialogProps) => {
|
export const DocumentMoveDialog = ({ documentId, open, onOpenChange }: DocumentMoveDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
|
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
|
||||||
|
|
||||||
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
|
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
|
||||||
|
|
||||||
const { mutateAsync: moveDocument, isPending } = trpc.document.moveDocumentToTeam.useMutation({
|
const { mutateAsync: moveDocument, isPending } = trpc.document.moveDocumentToTeam.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
router.refresh();
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Document moved`),
|
title: _(msg`Document moved`),
|
||||||
description: _(msg`The document has been successfully moved to the selected team.`),
|
description: _(msg`The document has been successfully moved to the selected team.`),
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@ -97,9 +94,7 @@ export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocum
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Avatar className="h-8 w-8">
|
<Avatar className="h-8 w-8">
|
||||||
{team.avatarImageId && (
|
{team.avatarImageId && (
|
||||||
<AvatarImage
|
<AvatarImage src={formatAvatarUrl(team.avatarImageId)} />
|
||||||
src={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${team.avatarImageId}`}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<AvatarFallback className="text-sm text-gray-400">
|
<AvatarFallback className="text-sm text-gray-400">
|
||||||
@ -1,19 +1,18 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Team } from '@prisma/client';
|
||||||
|
import { type Document, type Recipient, SigningStatus } from '@prisma/client';
|
||||||
import { History } from 'lucide-react';
|
import { History } from 'lucide-react';
|
||||||
import { useSession } from 'next-auth/react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
||||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { Team } from '@documenso/prisma/client';
|
|
||||||
import { type Document, type Recipient, SigningStatus } from '@documenso/prisma/client';
|
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -37,16 +36,17 @@ import {
|
|||||||
} from '@documenso/ui/primitives/form/form';
|
} from '@documenso/ui/primitives/form/form';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { StackAvatar } from '~/components/(dashboard)/avatar/stack-avatar';
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
|
import { StackAvatar } from '../general/stack-avatar';
|
||||||
|
|
||||||
const FORM_ID = 'resend-email';
|
const FORM_ID = 'resend-email';
|
||||||
|
|
||||||
export type ResendDocumentActionItemProps = {
|
export type DocumentResendDialogProps = {
|
||||||
document: Document & {
|
document: Document & {
|
||||||
team: Pick<Team, 'id' | 'url'> | null;
|
team: Pick<Team, 'id' | 'url'> | null;
|
||||||
};
|
};
|
||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
team?: Pick<Team, 'id' | 'url'>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZResendDocumentFormSchema = z.object({
|
export const ZResendDocumentFormSchema = z.object({
|
||||||
@ -57,17 +57,15 @@ export const ZResendDocumentFormSchema = z.object({
|
|||||||
|
|
||||||
export type TResendDocumentFormSchema = z.infer<typeof ZResendDocumentFormSchema>;
|
export type TResendDocumentFormSchema = z.infer<typeof ZResendDocumentFormSchema>;
|
||||||
|
|
||||||
export const ResendDocumentActionItem = ({
|
export const DocumentResendDialog = ({ document, recipients }: DocumentResendDialogProps) => {
|
||||||
document,
|
const { user } = useSession();
|
||||||
recipients,
|
const team = useOptionalCurrentTeam();
|
||||||
team,
|
|
||||||
}: ResendDocumentActionItemProps) => {
|
|
||||||
const { data: session } = useSession();
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const isOwner = document.userId === session?.user?.id;
|
const isOwner = document.userId === user.id;
|
||||||
const isCurrentTeamDocument = team && document.team?.url === team.url;
|
const isCurrentTeamDocument = team && document.team?.url === team.url;
|
||||||
|
|
||||||
const isDisabled =
|
const isDisabled =
|
||||||
@ -1,10 +1,9 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { startRegistration } from '@simplewebauthn/browser';
|
import { startRegistration } from '@simplewebauthn/browser';
|
||||||
import { KeyRoundIcon } from 'lucide-react';
|
import { KeyRoundIcon } from 'lucide-react';
|
||||||
@ -38,7 +37,7 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type CreatePasskeyDialogProps = {
|
export type PasskeyCreateDialogProps = {
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
@ -51,7 +50,7 @@ type TCreatePasskeyFormSchema = z.infer<typeof ZCreatePasskeyFormSchema>;
|
|||||||
|
|
||||||
const parser = new UAParser();
|
const parser = new UAParser();
|
||||||
|
|
||||||
export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePasskeyDialogProps) => {
|
export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCreateDialogProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [formError, setFormError] = useState<string | null>(null);
|
const [formError, setFormError] = useState<string | null>(null);
|
||||||
|
|
||||||
@ -1,18 +1,17 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Plural, Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Plural, Trans } from '@lingui/react/macro';
|
||||||
|
import type { Template, TemplateDirectLink } from '@prisma/client';
|
||||||
|
import { TemplateType } from '@prisma/client';
|
||||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { CheckCircle2Icon, CircleIcon } from 'lucide-react';
|
import { CheckCircle2Icon, CircleIcon } from 'lucide-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { P, match } from 'ts-pattern';
|
import { P, match } from 'ts-pattern';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { Template, TemplateDirectLink } from '@documenso/prisma/client';
|
|
||||||
import { TemplateType } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import {
|
import {
|
||||||
MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH,
|
MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH,
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { Loader, TagIcon } from 'lucide-react';
|
import { Loader, TagIcon } from 'lucide-react';
|
||||||
@ -20,18 +21,18 @@ import {
|
|||||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type CreateTeamCheckoutDialogProps = {
|
export type TeamCheckoutCreateDialogProps = {
|
||||||
pendingTeamId: number | null;
|
pendingTeamId: number | null;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
|
|
||||||
const MotionCard = motion(Card);
|
const MotionCard = motion(Card);
|
||||||
|
|
||||||
export const CreateTeamCheckoutDialog = ({
|
export const TeamCheckoutCreateDialog = ({
|
||||||
pendingTeamId,
|
pendingTeamId,
|
||||||
onClose,
|
onClose,
|
||||||
...props
|
...props
|
||||||
}: CreateTeamCheckoutDialogProps) => {
|
}: TeamCheckoutCreateDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@ -1,18 +1,17 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useSearchParams } from 'react-router';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { ZCreateTeamMutationSchema } from '@documenso/trpc/server/team-router/schema';
|
import { ZCreateTeamMutationSchema } from '@documenso/trpc/server/team-router/schema';
|
||||||
@ -37,7 +36,7 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type CreateTeamDialogProps = {
|
export type TeamCreateDialogProps = {
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
|
|
||||||
@ -48,12 +47,12 @@ const ZCreateTeamFormSchema = ZCreateTeamMutationSchema.pick({
|
|||||||
|
|
||||||
type TCreateTeamFormSchema = z.infer<typeof ZCreateTeamFormSchema>;
|
type TCreateTeamFormSchema = z.infer<typeof ZCreateTeamFormSchema>;
|
||||||
|
|
||||||
export const CreateTeamDialog = ({ trigger, ...props }: CreateTeamDialogProps) => {
|
export const TeamCreateDialog = ({ trigger, ...props }: TeamCreateDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const searchParams = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const updateSearchParams = useUpdateSearchParams();
|
const updateSearchParams = useUpdateSearchParams();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@ -80,7 +79,7 @@ export const CreateTeamDialog = ({ trigger, ...props }: CreateTeamDialogProps) =
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|
||||||
if (response.paymentRequired) {
|
if (response.paymentRequired) {
|
||||||
router.push(`/settings/teams?tab=pending&checkout=${response.pendingTeamId}`);
|
await navigate(`/settings/teams?tab=pending&checkout=${response.pendingTeamId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,7 +200,7 @@ export const CreateTeamDialog = ({ trigger, ...props }: CreateTeamDialogProps) =
|
|||||||
{!form.formState.errors.teamUrl && (
|
{!form.formState.errors.teamUrl && (
|
||||||
<span className="text-foreground/50 text-xs font-normal">
|
<span className="text-foreground/50 text-xs font-normal">
|
||||||
{field.value ? (
|
{field.value ? (
|
||||||
`${WEBAPP_BASE_URL}/t/${field.value}`
|
`${NEXT_PUBLIC_WEBAPP_URL()}/t/${field.value}`
|
||||||
) : (
|
) : (
|
||||||
<Trans>A unique URL to identify your team</Trans>
|
<Trans>A unique URL to identify your team</Trans>
|
||||||
)}
|
)}
|
||||||
@ -1,13 +1,11 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
@ -34,14 +32,14 @@ import { Input } from '@documenso/ui/primitives/input';
|
|||||||
import type { Toast } from '@documenso/ui/primitives/use-toast';
|
import type { Toast } from '@documenso/ui/primitives/use-toast';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DeleteTeamDialogProps = {
|
export type TeamDeleteDialogProps = {
|
||||||
teamId: number;
|
teamId: number;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteTeamDialog = ({ trigger, teamId, teamName }: DeleteTeamDialogProps) => {
|
export const TeamDeleteDialog = ({ trigger, teamId, teamName }: TeamDeleteDialogProps) => {
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -74,9 +72,9 @@ export const DeleteTeamDialog = ({ trigger, teamId, teamName }: DeleteTeamDialog
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
setOpen(false);
|
await navigate('/settings/teams');
|
||||||
|
|
||||||
router.push('/settings/teams');
|
setOpen(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -1,15 +1,13 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
@ -36,7 +34,7 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type AddTeamEmailDialogProps = {
|
export type TeamEmailAddDialogProps = {
|
||||||
teamId: number;
|
teamId: number;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
@ -48,13 +46,12 @@ const ZCreateTeamEmailFormSchema = ZCreateTeamEmailVerificationMutationSchema.pi
|
|||||||
|
|
||||||
type TCreateTeamEmailFormSchema = z.infer<typeof ZCreateTeamEmailFormSchema>;
|
type TCreateTeamEmailFormSchema = z.infer<typeof ZCreateTeamEmailFormSchema>;
|
||||||
|
|
||||||
export const AddTeamEmailDialog = ({ teamId, trigger, ...props }: AddTeamEmailDialogProps) => {
|
export const TeamEmailAddDialog = ({ teamId, trigger, ...props }: TeamEmailAddDialogProps) => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const form = useForm<TCreateTeamEmailFormSchema>({
|
const form = useForm<TCreateTeamEmailFormSchema>({
|
||||||
resolver: zodResolver(ZCreateTeamEmailFormSchema),
|
resolver: zodResolver(ZCreateTeamEmailFormSchema),
|
||||||
@ -81,7 +78,7 @@ export const AddTeamEmailDialog = ({ teamId, trigger, ...props }: AddTeamEmailDi
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
await revalidate();
|
||||||
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -1,15 +1,13 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Prisma } from '@prisma/client';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { Prisma } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert } from '@documenso/ui/primitives/alert';
|
import { Alert } from '@documenso/ui/primitives/alert';
|
||||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||||
@ -25,7 +23,7 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type RemoveTeamEmailDialogProps = {
|
export type TeamEmailDeleteDialogProps = {
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
team: Prisma.TeamGetPayload<{
|
team: Prisma.TeamGetPayload<{
|
||||||
@ -42,13 +40,12 @@ export type RemoveTeamEmailDialogProps = {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RemoveTeamEmailDialog = ({ trigger, teamName, team }: RemoveTeamEmailDialogProps) => {
|
export const TeamEmailDeleteDialog = ({ trigger, teamName, team }: TeamEmailDeleteDialogProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { mutateAsync: deleteTeamEmail, isPending: isDeletingTeamEmail } =
|
const { mutateAsync: deleteTeamEmail, isPending: isDeletingTeamEmail } =
|
||||||
trpc.team.deleteTeamEmail.useMutation({
|
trpc.team.deleteTeamEmail.useMutation({
|
||||||
@ -97,7 +94,7 @@ export const RemoveTeamEmailDialog = ({ trigger, teamName, team }: RemoveTeamEma
|
|||||||
await deleteTeamEmailVerification({ teamId: team.id });
|
await deleteTeamEmailVerification({ teamId: team.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
router.refresh();
|
await revalidate();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -127,7 +124,7 @@ export const RemoveTeamEmailDialog = ({ trigger, teamName, team }: RemoveTeamEma
|
|||||||
<Alert variant="neutral" padding="tight">
|
<Alert variant="neutral" padding="tight">
|
||||||
<AvatarWithText
|
<AvatarWithText
|
||||||
avatarClass="h-12 w-12"
|
avatarClass="h-12 w-12"
|
||||||
avatarSrc={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${team.avatarImageId}`}
|
avatarSrc={formatAvatarUrl(team.avatarImageId)}
|
||||||
avatarFallback={extractInitials(
|
avatarFallback={extractInitials(
|
||||||
(team.teamEmail?.name || team.emailVerification?.name) ?? '',
|
(team.teamEmail?.name || team.emailVerification?.name) ?? '',
|
||||||
)}
|
)}
|
||||||
@ -1,17 +1,15 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { TeamEmail } from '@prisma/client';
|
||||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { TeamEmail } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -34,7 +32,7 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type UpdateTeamEmailDialogProps = {
|
export type TeamEmailUpdateDialogProps = {
|
||||||
teamEmail: TeamEmail;
|
teamEmail: TeamEmail;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
@ -45,17 +43,16 @@ const ZUpdateTeamEmailFormSchema = z.object({
|
|||||||
|
|
||||||
type TUpdateTeamEmailFormSchema = z.infer<typeof ZUpdateTeamEmailFormSchema>;
|
type TUpdateTeamEmailFormSchema = z.infer<typeof ZUpdateTeamEmailFormSchema>;
|
||||||
|
|
||||||
export const UpdateTeamEmailDialog = ({
|
export const TeamEmailUpdateDialog = ({
|
||||||
teamEmail,
|
teamEmail,
|
||||||
trigger,
|
trigger,
|
||||||
...props
|
...props
|
||||||
}: UpdateTeamEmailDialogProps) => {
|
}: TeamEmailUpdateDialogProps) => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const form = useForm<TUpdateTeamEmailFormSchema>({
|
const form = useForm<TUpdateTeamEmailFormSchema>({
|
||||||
resolver: zodResolver(ZUpdateTeamEmailFormSchema),
|
resolver: zodResolver(ZUpdateTeamEmailFormSchema),
|
||||||
@ -81,7 +78,7 @@ export const UpdateTeamEmailDialog = ({
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
await revalidate();
|
||||||
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -1,13 +1,12 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { TeamMemberRole } from '@prisma/client';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
|
||||||
import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
|
import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
|
||||||
import type { TeamMemberRole } from '@documenso/prisma/client';
|
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert } from '@documenso/ui/primitives/alert';
|
import { Alert } from '@documenso/ui/primitives/alert';
|
||||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||||
@ -23,7 +22,7 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type LeaveTeamDialogProps = {
|
export type TeamLeaveDialogProps = {
|
||||||
teamId: number;
|
teamId: number;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
teamAvatarImageId?: string | null;
|
teamAvatarImageId?: string | null;
|
||||||
@ -31,13 +30,13 @@ export type LeaveTeamDialogProps = {
|
|||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LeaveTeamDialog = ({
|
export const TeamLeaveDialog = ({
|
||||||
trigger,
|
trigger,
|
||||||
teamId,
|
teamId,
|
||||||
teamName,
|
teamName,
|
||||||
teamAvatarImageId,
|
teamAvatarImageId,
|
||||||
role,
|
role,
|
||||||
}: LeaveTeamDialogProps) => {
|
}: TeamLeaveDialogProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -89,7 +88,7 @@ export const LeaveTeamDialog = ({
|
|||||||
<Alert variant="neutral" padding="tight">
|
<Alert variant="neutral" padding="tight">
|
||||||
<AvatarWithText
|
<AvatarWithText
|
||||||
avatarClass="h-12 w-12"
|
avatarClass="h-12 w-12"
|
||||||
avatarSrc={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${teamAvatarImageId}`}
|
avatarSrc={formatAvatarUrl(teamAvatarImageId)}
|
||||||
avatarFallback={teamName.slice(0, 1).toUpperCase()}
|
avatarFallback={teamName.slice(0, 1).toUpperCase()}
|
||||||
primaryText={teamName}
|
primaryText={teamName}
|
||||||
secondaryText={_(TEAM_MEMBER_ROLE_MAP[role])}
|
secondaryText={_(TEAM_MEMBER_ROLE_MAP[role])}
|
||||||
@ -1,9 +1,8 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert } from '@documenso/ui/primitives/alert';
|
import { Alert } from '@documenso/ui/primitives/alert';
|
||||||
@ -20,7 +19,7 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DeleteTeamMemberDialogProps = {
|
export type TeamMemberDeleteDialogProps = {
|
||||||
teamId: number;
|
teamId: number;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
teamMemberId: number;
|
teamMemberId: number;
|
||||||
@ -29,14 +28,14 @@ export type DeleteTeamMemberDialogProps = {
|
|||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteTeamMemberDialog = ({
|
export const TeamMemberDeleteDialog = ({
|
||||||
trigger,
|
trigger,
|
||||||
teamId,
|
teamId,
|
||||||
teamName,
|
teamName,
|
||||||
teamMemberId,
|
teamMemberId,
|
||||||
teamMemberName,
|
teamMemberName,
|
||||||
teamMemberEmail,
|
teamMemberEmail,
|
||||||
}: DeleteTeamMemberDialogProps) => {
|
}: TeamMemberDeleteDialogProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -1,10 +1,10 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { TeamMemberRole } from '@prisma/client';
|
||||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { Download, Mail, MailIcon, PlusCircle, Trash, Upload, UsersIcon } from 'lucide-react';
|
import { Download, Mail, MailIcon, PlusCircle, Trash, Upload, UsersIcon } from 'lucide-react';
|
||||||
import Papa, { type ParseResult } from 'papaparse';
|
import Papa, { type ParseResult } from 'papaparse';
|
||||||
@ -13,7 +13,6 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
||||||
import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
|
import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
|
||||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { ZCreateTeamMemberInvitesMutationSchema } from '@documenso/trpc/server/team-router/schema';
|
import { ZCreateTeamMemberInvitesMutationSchema } from '@documenso/trpc/server/team-router/schema';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@ -47,9 +46,9 @@ import {
|
|||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type InviteTeamMembersDialogProps = {
|
import { useCurrentTeam } from '~/providers/team';
|
||||||
currentUserTeamRole: TeamMemberRole;
|
|
||||||
teamId: number;
|
export type TeamMemberInviteDialogProps = {
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
|
|
||||||
@ -96,12 +95,7 @@ const ZImportTeamMemberSchema = z.array(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const InviteTeamMembersDialog = ({
|
export const TeamMemberInviteDialog = ({ trigger, ...props }: TeamMemberInviteDialogProps) => {
|
||||||
currentUserTeamRole,
|
|
||||||
teamId,
|
|
||||||
trigger,
|
|
||||||
...props
|
|
||||||
}: InviteTeamMembersDialogProps) => {
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [invitationType, setInvitationType] = useState<TabTypes>('INDIVIDUAL');
|
const [invitationType, setInvitationType] = useState<TabTypes>('INDIVIDUAL');
|
||||||
@ -109,6 +103,8 @@ export const InviteTeamMembersDialog = ({
|
|||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const team = useCurrentTeam();
|
||||||
|
|
||||||
const form = useForm<TInviteTeamMembersFormSchema>({
|
const form = useForm<TInviteTeamMembersFormSchema>({
|
||||||
resolver: zodResolver(ZInviteTeamMembersFormSchema),
|
resolver: zodResolver(ZInviteTeamMembersFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -142,7 +138,7 @@ export const InviteTeamMembersDialog = ({
|
|||||||
const onFormSubmit = async ({ invitations }: TInviteTeamMembersFormSchema) => {
|
const onFormSubmit = async ({ invitations }: TInviteTeamMembersFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await createTeamMemberInvites({
|
await createTeamMemberInvites({
|
||||||
teamId,
|
teamId: team.id,
|
||||||
invitations,
|
invitations,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -204,7 +200,7 @@ export const InviteTeamMembersDialog = ({
|
|||||||
|
|
||||||
setInvitationType('INDIVIDUAL');
|
setInvitationType('INDIVIDUAL');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err.message);
|
console.error(err);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Something went wrong`),
|
title: _(msg`Something went wrong`),
|
||||||
@ -325,11 +321,13 @@ export const InviteTeamMembersDialog = ({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
||||||
<SelectContent position="popper">
|
<SelectContent position="popper">
|
||||||
{TEAM_MEMBER_ROLE_HIERARCHY[currentUserTeamRole].map((role) => (
|
{TEAM_MEMBER_ROLE_HIERARCHY[team.currentTeamMember.role].map(
|
||||||
<SelectItem key={role} value={role}>
|
(role) => (
|
||||||
{_(TEAM_MEMBER_ROLE_MAP[role]) ?? role}
|
<SelectItem key={role} value={role}>
|
||||||
</SelectItem>
|
{_(TEAM_MEMBER_ROLE_MAP[role]) ?? role}
|
||||||
))}
|
</SelectItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -1,17 +1,16 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { TeamMemberRole } from '@prisma/client';
|
||||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
|
import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
|
||||||
import { isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
|
import { isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
|
||||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -40,7 +39,7 @@ import {
|
|||||||
} from '@documenso/ui/primitives/select';
|
} from '@documenso/ui/primitives/select';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type UpdateTeamMemberDialogProps = {
|
export type TeamMemberUpdateDialogProps = {
|
||||||
currentUserTeamRole: TeamMemberRole;
|
currentUserTeamRole: TeamMemberRole;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
teamId: number;
|
teamId: number;
|
||||||
@ -55,7 +54,7 @@ const ZUpdateTeamMemberFormSchema = z.object({
|
|||||||
|
|
||||||
type ZUpdateTeamMemberSchema = z.infer<typeof ZUpdateTeamMemberFormSchema>;
|
type ZUpdateTeamMemberSchema = z.infer<typeof ZUpdateTeamMemberFormSchema>;
|
||||||
|
|
||||||
export const UpdateTeamMemberDialog = ({
|
export const TeamMemberUpdateDialog = ({
|
||||||
currentUserTeamRole,
|
currentUserTeamRole,
|
||||||
trigger,
|
trigger,
|
||||||
teamId,
|
teamId,
|
||||||
@ -63,7 +62,7 @@ export const UpdateTeamMemberDialog = ({
|
|||||||
teamMemberName,
|
teamMemberName,
|
||||||
teamMemberRole,
|
teamMemberRole,
|
||||||
...props
|
...props
|
||||||
}: UpdateTeamMemberDialogProps) => {
|
}: TeamMemberUpdateDialogProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -1,14 +1,12 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||||
@ -42,24 +40,24 @@ import {
|
|||||||
} from '@documenso/ui/primitives/select';
|
} from '@documenso/ui/primitives/select';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type TransferTeamDialogProps = {
|
export type TeamTransferDialogProps = {
|
||||||
teamId: number;
|
teamId: number;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
ownerUserId: number;
|
ownerUserId: number;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TransferTeamDialog = ({
|
export const TeamTransferDialog = ({
|
||||||
trigger,
|
trigger,
|
||||||
teamId,
|
teamId,
|
||||||
teamName,
|
teamName,
|
||||||
ownerUserId,
|
ownerUserId,
|
||||||
}: TransferTeamDialogProps) => {
|
}: TeamTransferDialogProps) => {
|
||||||
const router = useRouter();
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const { mutateAsync: requestTeamOwnershipTransfer } =
|
const { mutateAsync: requestTeamOwnershipTransfer } =
|
||||||
trpc.team.requestTeamOwnershipTransfer.useMutation();
|
trpc.team.requestTeamOwnershipTransfer.useMutation();
|
||||||
@ -102,7 +100,7 @@ export const TransferTeamDialog = ({
|
|||||||
clearPaymentMethods,
|
clearPaymentMethods,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
await revalidate();
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Success`),
|
title: _(msg`Success`),
|
||||||
@ -1,15 +1,12 @@
|
|||||||
'use client';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { FilePlus, Loader } from 'lucide-react';
|
import { FilePlus, Loader } from 'lucide-react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -26,21 +23,21 @@ import {
|
|||||||
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
|
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
type NewTemplateDialogProps = {
|
type TemplateCreateDialogProps = {
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
templateRootPath: string;
|
templateRootPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps) => {
|
export const TemplateCreateDialog = ({ templateRootPath }: TemplateCreateDialogProps) => {
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { data: session } = useSession();
|
const { user } = useSession();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
|
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
|
||||||
|
|
||||||
const [showNewTemplateDialog, setShowNewTemplateDialog] = useState(false);
|
const [showTemplateCreateDialog, setShowTemplateCreateDialog] = useState(false);
|
||||||
const [isUploadingFile, setIsUploadingFile] = useState(false);
|
const [isUploadingFile, setIsUploadingFile] = useState(false);
|
||||||
|
|
||||||
const onFileDrop = async (file: File) => {
|
const onFileDrop = async (file: File) => {
|
||||||
@ -51,15 +48,11 @@ export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps)
|
|||||||
setIsUploadingFile(true);
|
setIsUploadingFile(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { type, data } = await putPdfFile(file);
|
const response = await putPdfFile(file);
|
||||||
const { id: templateDocumentDataId } = await createDocumentData({
|
|
||||||
type,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { id } = await createTemplate({
|
const { id } = await createTemplate({
|
||||||
title: file.name,
|
title: file.name,
|
||||||
templateDocumentDataId,
|
templateDocumentDataId: response.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@ -70,9 +63,9 @@ export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps)
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
setShowNewTemplateDialog(false);
|
setShowTemplateCreateDialog(false);
|
||||||
|
|
||||||
router.push(`${templateRootPath}/${id}/edit`);
|
await navigate(`${templateRootPath}/${id}/edit`);
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Something went wrong`),
|
title: _(msg`Something went wrong`),
|
||||||
@ -86,11 +79,11 @@ export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps)
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={showNewTemplateDialog}
|
open={showTemplateCreateDialog}
|
||||||
onOpenChange={(value) => !isUploadingFile && setShowNewTemplateDialog(value)}
|
onOpenChange={(value) => !isUploadingFile && setShowTemplateCreateDialog(value)}
|
||||||
>
|
>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className="cursor-pointer" disabled={!session?.user.emailVerified}>
|
<Button className="cursor-pointer" disabled={!user.emailVerified}>
|
||||||
<FilePlus className="-ml-1 mr-2 h-4 w-4" />
|
<FilePlus className="-ml-1 mr-2 h-4 w-4" />
|
||||||
<Trans>New Template</Trans>
|
<Trans>New Template</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import { useRouter } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -15,22 +14,25 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
type DeleteTemplateDialogProps = {
|
type TemplateDeleteDialogProps = {
|
||||||
id: number;
|
id: number;
|
||||||
teamId?: number;
|
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
|
onDelete?: () => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateDialogProps) => {
|
export const TemplateDeleteDialog = ({
|
||||||
const router = useRouter();
|
id,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onDelete,
|
||||||
|
}: TemplateDeleteDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { mutateAsync: deleteTemplate, isPending } = trpcReact.template.deleteTemplate.useMutation({
|
const { mutateAsync: deleteTemplate, isPending } = trpcReact.template.deleteTemplate.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: async () => {
|
||||||
router.refresh();
|
await onDelete?.();
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Template deleted`),
|
title: _(msg`Template deleted`),
|
||||||
@ -1,14 +1,12 @@
|
|||||||
'use client';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Recipient, Template, TemplateDirectLink } from '@prisma/client';
|
||||||
import { Trans } from '@lingui/macro';
|
|
||||||
import { LinkIcon } from 'lucide-react';
|
import { LinkIcon } from 'lucide-react';
|
||||||
|
|
||||||
import type { Recipient, Template, TemplateDirectLink } from '@documenso/prisma/client';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
import { TemplateDirectLinkDialog } from '../template-direct-link-dialog';
|
import { TemplateDirectLinkDialog } from '~/components/dialogs/template-direct-link-dialog';
|
||||||
|
|
||||||
export type TemplateDirectLinkDialogWrapperProps = {
|
export type TemplateDirectLinkDialogWrapperProps = {
|
||||||
template: Template & { directLink?: TemplateDirectLink | null; recipients: Recipient[] };
|
template: Template & { directLink?: TemplateDirectLink | null; recipients: Recipient[] };
|
||||||
@ -1,11 +1,16 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import {
|
||||||
|
type Recipient,
|
||||||
|
RecipientRole,
|
||||||
|
type Template,
|
||||||
|
type TemplateDirectLink,
|
||||||
|
} from '@prisma/client';
|
||||||
import { CircleDotIcon, CircleIcon, ClipboardCopyIcon, InfoIcon, LoaderIcon } from 'lucide-react';
|
import { CircleDotIcon, CircleIcon, ClipboardCopyIcon, InfoIcon, LoaderIcon } from 'lucide-react';
|
||||||
|
import { Link, useRevalidator } from 'react-router';
|
||||||
import { P, match } from 'ts-pattern';
|
import { P, match } from 'ts-pattern';
|
||||||
|
|
||||||
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||||
@ -14,12 +19,6 @@ import { DIRECT_TEMPLATE_RECIPIENT_EMAIL } from '@documenso/lib/constants/direct
|
|||||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||||
import { DIRECT_TEMPLATE_DOCUMENTATION } from '@documenso/lib/constants/template';
|
import { DIRECT_TEMPLATE_DOCUMENTATION } from '@documenso/lib/constants/template';
|
||||||
import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
|
import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
|
||||||
import {
|
|
||||||
type Recipient,
|
|
||||||
RecipientRole,
|
|
||||||
type Template,
|
|
||||||
type TemplateDirectLink,
|
|
||||||
} from '@documenso/prisma/client';
|
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
|
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
@ -65,9 +64,9 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { quota, remaining } = useLimits();
|
const { quota, remaining } = useLimits();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const [, copy] = useCopyToClipboard();
|
const [, copy] = useCopyToClipboard();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [isEnabled, setIsEnabled] = useState(template.directLink?.enabled ?? false);
|
const [isEnabled, setIsEnabled] = useState(template.directLink?.enabled ?? false);
|
||||||
const [token, setToken] = useState(template.directLink?.token ?? null);
|
const [token, setToken] = useState(template.directLink?.token ?? null);
|
||||||
@ -86,12 +85,12 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
isPending: isCreatingTemplateDirectLink,
|
isPending: isCreatingTemplateDirectLink,
|
||||||
reset: resetCreateTemplateDirectLink,
|
reset: resetCreateTemplateDirectLink,
|
||||||
} = trpcReact.template.createTemplateDirectLink.useMutation({
|
} = trpcReact.template.createTemplateDirectLink.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: async (data) => {
|
||||||
|
await revalidate();
|
||||||
|
|
||||||
setToken(data.token);
|
setToken(data.token);
|
||||||
setIsEnabled(data.enabled);
|
setIsEnabled(data.enabled);
|
||||||
setCurrentStep('MANAGE');
|
setCurrentStep('MANAGE');
|
||||||
|
|
||||||
router.refresh();
|
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
setSelectedRecipientId(null);
|
setSelectedRecipientId(null);
|
||||||
@ -106,7 +105,9 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
|
|
||||||
const { mutateAsync: toggleTemplateDirectLink, isPending: isTogglingTemplateAccess } =
|
const { mutateAsync: toggleTemplateDirectLink, isPending: isTogglingTemplateAccess } =
|
||||||
trpcReact.template.toggleTemplateDirectLink.useMutation({
|
trpcReact.template.toggleTemplateDirectLink.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: async (data) => {
|
||||||
|
await revalidate();
|
||||||
|
|
||||||
const enabledDescription = msg`Direct link signing has been enabled`;
|
const enabledDescription = msg`Direct link signing has been enabled`;
|
||||||
const disabledDescription = msg`Direct link signing has been disabled`;
|
const disabledDescription = msg`Direct link signing has been disabled`;
|
||||||
|
|
||||||
@ -129,7 +130,9 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
|
|
||||||
const { mutateAsync: deleteTemplateDirectLink, isPending: isDeletingTemplateDirectLink } =
|
const { mutateAsync: deleteTemplateDirectLink, isPending: isDeletingTemplateDirectLink } =
|
||||||
trpcReact.template.deleteTemplateDirectLink.useMutation({
|
trpcReact.template.deleteTemplateDirectLink.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: async () => {
|
||||||
|
await revalidate();
|
||||||
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
setToken(null);
|
setToken(null);
|
||||||
|
|
||||||
@ -139,7 +142,6 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
|
||||||
setToken(null);
|
setToken(null);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
@ -231,7 +233,7 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
templates.{' '}
|
templates.{' '}
|
||||||
<Link
|
<Link
|
||||||
className="mt-1 block underline underline-offset-4"
|
className="mt-1 block underline underline-offset-4"
|
||||||
href="/settings/billing"
|
to="/settings/billing"
|
||||||
>
|
>
|
||||||
Upgrade your account to continue!
|
Upgrade your account to continue!
|
||||||
</Link>
|
</Link>
|
||||||
@ -432,7 +434,7 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
await toggleTemplateDirectLink({
|
await toggleTemplateDirectLink({
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
enabled: isEnabled,
|
enabled: isEnabled,
|
||||||
}).catch((e) => null);
|
}).catch(() => null);
|
||||||
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
}}
|
}}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import { useRouter } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -15,28 +14,23 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
type DuplicateTemplateDialogProps = {
|
type TemplateDuplicateDialogProps = {
|
||||||
id: number;
|
id: number;
|
||||||
teamId?: number;
|
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DuplicateTemplateDialog = ({
|
export const TemplateDuplicateDialog = ({
|
||||||
id,
|
id,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: DuplicateTemplateDialogProps) => {
|
}: TemplateDuplicateDialogProps) => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { mutateAsync: duplicateTemplate, isPending } =
|
const { mutateAsync: duplicateTemplate, isPending } =
|
||||||
trpcReact.template.duplicateTemplate.useMutation({
|
trpcReact.template.duplicateTemplate.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
router.refresh();
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Template duplicated`),
|
title: _(msg`Template duplicated`),
|
||||||
description: _(msg`Your template has been duplicated successfully.`),
|
description: _(msg`Your template has been duplicated successfully.`),
|
||||||
@ -1,13 +1,12 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
|
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -28,29 +27,46 @@ import {
|
|||||||
} from '@documenso/ui/primitives/select';
|
} from '@documenso/ui/primitives/select';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
type MoveTemplateDialogProps = {
|
type TemplateMoveDialogProps = {
|
||||||
templateId: number;
|
templateId: number;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
|
onMove?: ({
|
||||||
|
templateId,
|
||||||
|
teamUrl,
|
||||||
|
}: {
|
||||||
|
templateId: number;
|
||||||
|
teamUrl: string;
|
||||||
|
}) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTemplateDialogProps) => {
|
export const TemplateMoveDialog = ({
|
||||||
const router = useRouter();
|
templateId,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onMove,
|
||||||
|
}: TemplateMoveDialogProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
|
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
|
||||||
|
|
||||||
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
|
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
|
||||||
|
|
||||||
const { mutateAsync: moveTemplate, isPending } = trpc.template.moveTemplateToTeam.useMutation({
|
const { mutateAsync: moveTemplate, isPending } = trpc.template.moveTemplateToTeam.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: async () => {
|
||||||
router.refresh();
|
const team = teams?.find((team) => team.id === selectedTeamId);
|
||||||
|
|
||||||
|
if (team) {
|
||||||
|
await onMove?.({ templateId, teamUrl: team.url });
|
||||||
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Template moved`),
|
title: _(msg`Template moved`),
|
||||||
description: _(msg`The template has been successfully moved to the selected team.`),
|
description: _(msg`The template has been successfully moved to the selected team.`),
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
@ -73,7 +89,7 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onMove = async () => {
|
const handleOnMove = async () => {
|
||||||
if (!selectedTeamId) {
|
if (!selectedTeamId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -108,9 +124,7 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Avatar className="h-8 w-8">
|
<Avatar className="h-8 w-8">
|
||||||
{team.avatarImageId && (
|
{team.avatarImageId && (
|
||||||
<AvatarImage
|
<AvatarImage src={formatAvatarUrl(team.avatarImageId)} />
|
||||||
src={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${team.avatarImageId}`}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<AvatarFallback className="text-sm text-gray-400">
|
<AvatarFallback className="text-sm text-gray-400">
|
||||||
@ -130,7 +144,11 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
|
|||||||
<Button variant="secondary" onClick={() => onOpenChange(false)}>
|
<Button variant="secondary" onClick={() => onOpenChange(false)}>
|
||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onMove} loading={isPending} disabled={!selectedTeamId || isPending}>
|
<Button
|
||||||
|
onClick={handleOnMove}
|
||||||
|
loading={isPending}
|
||||||
|
disabled={!selectedTeamId || isPending}
|
||||||
|
>
|
||||||
{isPending ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
|
{isPending ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@ -1,14 +1,14 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Recipient } from '@prisma/client';
|
||||||
|
import { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/client';
|
||||||
import { InfoIcon, Plus, Upload, X } from 'lucide-react';
|
import { InfoIcon, Plus, Upload, X } from 'lucide-react';
|
||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||||
@ -18,8 +18,6 @@ import {
|
|||||||
} from '@documenso/lib/constants/template';
|
} from '@documenso/lib/constants/template';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
|
||||||
import { DocumentDistributionMethod, DocumentSigningOrder } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -94,7 +92,7 @@ const ZAddRecipientsForNewDocumentSchema = z
|
|||||||
|
|
||||||
type TAddRecipientsForNewDocumentSchema = z.infer<typeof ZAddRecipientsForNewDocumentSchema>;
|
type TAddRecipientsForNewDocumentSchema = z.infer<typeof ZAddRecipientsForNewDocumentSchema>;
|
||||||
|
|
||||||
export type UseTemplateDialogProps = {
|
export type TemplateUseDialogProps = {
|
||||||
templateId: number;
|
templateId: number;
|
||||||
templateSigningOrder?: DocumentSigningOrder | null;
|
templateSigningOrder?: DocumentSigningOrder | null;
|
||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
@ -103,19 +101,19 @@ export type UseTemplateDialogProps = {
|
|||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function UseTemplateDialog({
|
export function TemplateUseDialog({
|
||||||
recipients,
|
recipients,
|
||||||
documentDistributionMethod = DocumentDistributionMethod.EMAIL,
|
documentDistributionMethod = DocumentDistributionMethod.EMAIL,
|
||||||
documentRootPath,
|
documentRootPath,
|
||||||
templateId,
|
templateId,
|
||||||
templateSigningOrder,
|
templateSigningOrder,
|
||||||
trigger,
|
trigger,
|
||||||
}: UseTemplateDialogProps) {
|
}: TemplateUseDialogProps) {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const form = useForm<TAddRecipientsForNewDocumentSchema>({
|
const form = useForm<TAddRecipientsForNewDocumentSchema>({
|
||||||
@ -179,7 +177,7 @@ export function UseTemplateDialog({
|
|||||||
documentPath += '?action=view-signing-links';
|
documentPath += '?action=view-signing-links';
|
||||||
}
|
}
|
||||||
|
|
||||||
router.push(documentPath);
|
await navigate(documentPath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -1,16 +1,13 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { ApiToken } from '@prisma/client';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { ApiToken } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -33,35 +30,33 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DeleteTokenDialogProps = {
|
export type TokenDeleteDialogProps = {
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
token: Pick<ApiToken, 'id' | 'name'>;
|
token: Pick<ApiToken, 'id' | 'name'>;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DeleteTokenDialog({
|
export default function TokenDeleteDialog({
|
||||||
teamId,
|
teamId,
|
||||||
token,
|
token,
|
||||||
onDelete,
|
onDelete,
|
||||||
children,
|
children,
|
||||||
}: DeleteTokenDialogProps) {
|
}: TokenDeleteDialogProps) {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const deleteMessage = _(msg`delete ${token.name}`);
|
const deleteMessage = _(msg`delete ${token.name}`);
|
||||||
|
|
||||||
const ZDeleteTokenDialogSchema = z.object({
|
const ZTokenDeleteDialogSchema = z.object({
|
||||||
tokenName: z.literal(deleteMessage, {
|
tokenName: z.literal(deleteMessage, {
|
||||||
errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
|
errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
type TDeleteTokenByIdMutationSchema = z.infer<typeof ZDeleteTokenDialogSchema>;
|
type TDeleteTokenByIdMutationSchema = z.infer<typeof ZTokenDeleteDialogSchema>;
|
||||||
|
|
||||||
const { mutateAsync: deleteTokenMutation } = trpc.apiToken.deleteTokenById.useMutation({
|
const { mutateAsync: deleteTokenMutation } = trpc.apiToken.deleteTokenById.useMutation({
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
@ -70,7 +65,7 @@ export default function DeleteTokenDialog({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<TDeleteTokenByIdMutationSchema>({
|
const form = useForm<TDeleteTokenByIdMutationSchema>({
|
||||||
resolver: zodResolver(ZDeleteTokenDialogSchema),
|
resolver: zodResolver(ZTokenDeleteDialogSchema),
|
||||||
values: {
|
values: {
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
},
|
},
|
||||||
@ -90,8 +85,6 @@ export default function DeleteTokenDialog({
|
|||||||
});
|
});
|
||||||
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
|
||||||
router.refresh();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
@ -1,12 +1,9 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
@ -39,22 +36,20 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
import { TriggerMultiSelectCombobox } from './trigger-multiselect-combobox';
|
import { WebhookMultiSelectCombobox } from '../general/webhook-multiselect-combobox';
|
||||||
|
|
||||||
const ZCreateWebhookFormSchema = ZCreateWebhookMutationSchema.omit({ teamId: true });
|
const ZCreateWebhookFormSchema = ZCreateWebhookMutationSchema.omit({ teamId: true });
|
||||||
|
|
||||||
type TCreateWebhookFormSchema = z.infer<typeof ZCreateWebhookFormSchema>;
|
type TCreateWebhookFormSchema = z.infer<typeof ZCreateWebhookFormSchema>;
|
||||||
|
|
||||||
export type CreateWebhookDialogProps = {
|
export type WebhookCreateDialogProps = {
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
|
|
||||||
export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogProps) => {
|
export const WebhookCreateDialog = ({ trigger, ...props }: WebhookCreateDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const team = useOptionalCurrentTeam();
|
const team = useOptionalCurrentTeam();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@ -94,8 +89,6 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr
|
|||||||
});
|
});
|
||||||
|
|
||||||
form.reset();
|
form.reset();
|
||||||
|
|
||||||
router.refresh();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
@ -191,7 +184,7 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr
|
|||||||
<Trans>Triggers</Trans>
|
<Trans>Triggers</Trans>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<TriggerMultiSelectCombobox
|
<WebhookMultiSelectCombobox
|
||||||
listValues={value}
|
listValues={value}
|
||||||
onChange={(values: string[]) => {
|
onChange={(values: string[]) => {
|
||||||
onChange(values);
|
onChange(values);
|
||||||
@ -1,16 +1,13 @@
|
|||||||
'use effect';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Webhook } from '@prisma/client';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { Webhook } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -35,18 +32,16 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
export type DeleteWebhookDialogProps = {
|
export type WebhookDeleteDialogProps = {
|
||||||
webhook: Pick<Webhook, 'id' | 'webhookUrl'>;
|
webhook: Pick<Webhook, 'id' | 'webhookUrl'>;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogProps) => {
|
export const WebhookDeleteDialog = ({ webhook, children }: WebhookDeleteDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const team = useOptionalCurrentTeam();
|
const team = useOptionalCurrentTeam();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@ -81,8 +76,6 @@ export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogPr
|
|||||||
});
|
});
|
||||||
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|
||||||
router.refresh();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
@ -1,20 +1,23 @@
|
|||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
|
||||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||||
|
|
||||||
import { Logo } from '~/components/branding/logo';
|
|
||||||
import { SignInForm } from '~/components/forms/signin';
|
import { SignInForm } from '~/components/forms/signin';
|
||||||
|
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||||
|
|
||||||
export type EmbedAuthenticateViewProps = {
|
export type EmbedAuthenticationRequiredProps = {
|
||||||
email?: string;
|
email?: string;
|
||||||
returnTo: string;
|
returnTo: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EmbedAuthenticateView = ({ email, returnTo }: EmbedAuthenticateViewProps) => {
|
export const EmbedAuthenticationRequired = ({
|
||||||
|
email,
|
||||||
|
returnTo,
|
||||||
|
}: EmbedAuthenticationRequiredProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[100dvh] w-full items-center justify-center">
|
<div className="flex min-h-[100dvh] w-full items-center justify-center">
|
||||||
<div className="flex w-full max-w-md flex-col">
|
<div className="flex w-full max-w-md flex-col">
|
||||||
<Logo className="h-8" />
|
<BrandingLogo className="h-8" />
|
||||||
|
|
||||||
<Alert className="mt-8" variant="warning">
|
<Alert className="mt-8" variant="warning">
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
@ -1,21 +1,19 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { type DocumentData, type Field, FieldType } from '@prisma/client';
|
||||||
|
import type { DocumentMeta, Recipient, Signature, TemplateMeta } from '@prisma/client';
|
||||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { useSearchParams } from 'react-router';
|
||||||
|
|
||||||
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||||
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import type { DocumentMeta, Recipient, Signature, TemplateMeta } from '@documenso/prisma/client';
|
|
||||||
import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type {
|
import type {
|
||||||
TRemovedSignedFieldWithTokenMutationSchema,
|
TRemovedSignedFieldWithTokenMutationSchema,
|
||||||
@ -31,15 +29,15 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
|||||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import type { DirectTemplateLocalField } from '~/app/(recipient)/d/[token]/sign-direct-template';
|
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
import { ZDirectTemplateEmbedDataSchema } from '~/types/embed-direct-template-schema';
|
||||||
import { Logo } from '~/components/branding/logo';
|
import { injectCss } from '~/utils/css-vars';
|
||||||
|
|
||||||
import { EmbedClientLoading } from '../../client-loading';
|
import type { DirectTemplateLocalField } from '../general/direct-template/direct-template-signing-form';
|
||||||
import { EmbedDocumentCompleted } from '../../completed';
|
import { useRequiredDocumentSigningContext } from '../general/document-signing/document-signing-provider';
|
||||||
import { EmbedDocumentFields } from '../../document-fields';
|
import { EmbedClientLoading } from './embed-client-loading';
|
||||||
import { injectCss } from '../../util';
|
import { EmbedDocumentCompleted } from './embed-document-completed';
|
||||||
import { ZDirectTemplateEmbedDataSchema } from './schema';
|
import { EmbedDocumentFields } from './embed-document-fields';
|
||||||
|
|
||||||
export type EmbedDirectTemplateClientPageProps = {
|
export type EmbedDirectTemplateClientPageProps = {
|
||||||
token: string;
|
token: string;
|
||||||
@ -65,7 +63,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
fullName,
|
fullName,
|
||||||
@ -76,7 +74,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
setEmail,
|
setEmail,
|
||||||
setSignature,
|
setSignature,
|
||||||
setSignatureValid,
|
setSignatureValid,
|
||||||
} = useRequiredSigningContext();
|
} = useRequiredDocumentSigningContext();
|
||||||
|
|
||||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||||
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
||||||
@ -496,7 +494,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
{!hidePoweredBy && (
|
{!hidePoweredBy && (
|
||||||
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
|
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
|
||||||
<span>Powered by</span>
|
<span>Powered by</span>
|
||||||
<Logo className="ml-2 inline-block h-[14px]" />
|
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Signature } from '@prisma/client';
|
||||||
|
|
||||||
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
|
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
|
||||||
import type { Signature } from '@documenso/prisma/client';
|
|
||||||
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
||||||
|
|
||||||
export type EmbedDocumentCompletedPageProps = {
|
export type EmbedDocumentCompletedPageProps = {
|
||||||
@ -1,5 +1,5 @@
|
|||||||
'use client';
|
import type { DocumentMeta, Recipient, TemplateMeta } from '@prisma/client';
|
||||||
|
import { type Field, FieldType } from '@prisma/client';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||||
@ -12,8 +12,6 @@ import {
|
|||||||
ZRadioFieldMeta,
|
ZRadioFieldMeta,
|
||||||
ZTextFieldMeta,
|
ZTextFieldMeta,
|
||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
|
|
||||||
import { type Field, FieldType } from '@documenso/prisma/client';
|
|
||||||
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
||||||
import type {
|
import type {
|
||||||
TRemovedSignedFieldWithTokenMutationSchema,
|
TRemovedSignedFieldWithTokenMutationSchema,
|
||||||
@ -21,16 +19,16 @@ import type {
|
|||||||
} from '@documenso/trpc/server/field-router/schema';
|
} from '@documenso/trpc/server/field-router/schema';
|
||||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||||
|
|
||||||
import { CheckboxField } from '~/app/(signing)/sign/[token]/checkbox-field';
|
import { DocumentSigningCheckboxField } from '~/components/general/document-signing/document-signing-checkbox-field';
|
||||||
import { DateField } from '~/app/(signing)/sign/[token]/date-field';
|
import { DocumentSigningDateField } from '~/components/general/document-signing/document-signing-date-field';
|
||||||
import { DropdownField } from '~/app/(signing)/sign/[token]/dropdown-field';
|
import { DocumentSigningDropdownField } from '~/components/general/document-signing/document-signing-dropdown-field';
|
||||||
import { EmailField } from '~/app/(signing)/sign/[token]/email-field';
|
import { DocumentSigningEmailField } from '~/components/general/document-signing/document-signing-email-field';
|
||||||
import { InitialsField } from '~/app/(signing)/sign/[token]/initials-field';
|
import { DocumentSigningInitialsField } from '~/components/general/document-signing/document-signing-initials-field';
|
||||||
import { NameField } from '~/app/(signing)/sign/[token]/name-field';
|
import { DocumentSigningNameField } from '~/components/general/document-signing/document-signing-name-field';
|
||||||
import { NumberField } from '~/app/(signing)/sign/[token]/number-field';
|
import { DocumentSigningNumberField } from '~/components/general/document-signing/document-signing-number-field';
|
||||||
import { RadioField } from '~/app/(signing)/sign/[token]/radio-field';
|
import { DocumentSigningRadioField } from '~/components/general/document-signing/document-signing-radio-field';
|
||||||
import { SignatureField } from '~/app/(signing)/sign/[token]/signature-field';
|
import { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-field';
|
||||||
import { TextField } from '~/app/(signing)/sign/[token]/text-field';
|
import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
|
||||||
|
|
||||||
export type EmbedDocumentFieldsProps = {
|
export type EmbedDocumentFieldsProps = {
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
@ -52,7 +50,7 @@ export const EmbedDocumentFields = ({
|
|||||||
{fields.map((field) =>
|
{fields.map((field) =>
|
||||||
match(field.type)
|
match(field.type)
|
||||||
.with(FieldType.SIGNATURE, () => (
|
.with(FieldType.SIGNATURE, () => (
|
||||||
<SignatureField
|
<DocumentSigningSignatureField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
@ -62,7 +60,7 @@ export const EmbedDocumentFields = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.INITIALS, () => (
|
.with(FieldType.INITIALS, () => (
|
||||||
<InitialsField
|
<DocumentSigningInitialsField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
@ -71,7 +69,7 @@ export const EmbedDocumentFields = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.NAME, () => (
|
.with(FieldType.NAME, () => (
|
||||||
<NameField
|
<DocumentSigningNameField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
@ -80,7 +78,7 @@ export const EmbedDocumentFields = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.DATE, () => (
|
.with(FieldType.DATE, () => (
|
||||||
<DateField
|
<DocumentSigningDateField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
@ -91,7 +89,7 @@ export const EmbedDocumentFields = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.EMAIL, () => (
|
.with(FieldType.EMAIL, () => (
|
||||||
<EmailField
|
<DocumentSigningEmailField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
@ -106,7 +104,7 @@ export const EmbedDocumentFields = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<DocumentSigningTextField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={fieldWithMeta}
|
field={fieldWithMeta}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
@ -122,7 +120,7 @@ export const EmbedDocumentFields = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NumberField
|
<DocumentSigningNumberField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={fieldWithMeta}
|
field={fieldWithMeta}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
@ -138,7 +136,7 @@ export const EmbedDocumentFields = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RadioField
|
<DocumentSigningRadioField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={fieldWithMeta}
|
field={fieldWithMeta}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
@ -154,7 +152,7 @@ export const EmbedDocumentFields = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CheckboxField
|
<DocumentSigningCheckboxField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={fieldWithMeta}
|
field={fieldWithMeta}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
@ -170,7 +168,7 @@ export const EmbedDocumentFields = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownField
|
<DocumentSigningDropdownField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={fieldWithMeta}
|
field={fieldWithMeta}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
@ -1,16 +1,15 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { DocumentMeta, Recipient, TemplateMeta } from '@prisma/client';
|
||||||
|
import { type DocumentData, type Field, FieldType } from '@prisma/client';
|
||||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||||
|
|
||||||
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
|
|
||||||
import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -22,14 +21,14 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
|||||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||||
import { Logo } from '~/components/branding/logo';
|
import { injectCss } from '~/utils/css-vars';
|
||||||
|
|
||||||
import { EmbedClientLoading } from '../../client-loading';
|
import { ZSignDocumentEmbedDataSchema } from '../../types/embed-document-sign-schema';
|
||||||
import { EmbedDocumentCompleted } from '../../completed';
|
import { useRequiredDocumentSigningContext } from '../general/document-signing/document-signing-provider';
|
||||||
import { EmbedDocumentFields } from '../../document-fields';
|
import { EmbedClientLoading } from './embed-client-loading';
|
||||||
import { injectCss } from '../../util';
|
import { EmbedDocumentCompleted } from './embed-document-completed';
|
||||||
import { ZSignDocumentEmbedDataSchema } from './schema';
|
import { EmbedDocumentFields } from './embed-document-fields';
|
||||||
|
|
||||||
export type EmbedSignDocumentClientPageProps = {
|
export type EmbedSignDocumentClientPageProps = {
|
||||||
token: string;
|
token: string;
|
||||||
@ -65,7 +64,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
setFullName,
|
setFullName,
|
||||||
setSignature,
|
setSignature,
|
||||||
setSignatureValid,
|
setSignatureValid,
|
||||||
} = useRequiredSigningContext();
|
} = useRequiredDocumentSigningContext();
|
||||||
|
|
||||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||||
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
||||||
@ -369,7 +368,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
{!hidePoweredBy && (
|
{!hidePoweredBy && (
|
||||||
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
|
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
|
||||||
<span>Powered by</span>
|
<span>Powered by</span>
|
||||||
<Logo className="ml-2 inline-block h-[14px]" />
|
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -1,14 +1,12 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { flushSync } from 'react-dom';
|
import { flushSync } from 'react-dom';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
@ -42,10 +40,9 @@ export const ZDisable2FAForm = z.object({
|
|||||||
export type TDisable2FAForm = z.infer<typeof ZDisable2FAForm>;
|
export type TDisable2FAForm = z.infer<typeof ZDisable2FAForm>;
|
||||||
|
|
||||||
export const DisableAuthenticatorAppDialog = () => {
|
export const DisableAuthenticatorAppDialog = () => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [twoFactorDisableMethod, setTwoFactorDisableMethod] = useState<'totp' | 'backup'>('totp');
|
const [twoFactorDisableMethod, setTwoFactorDisableMethod] = useState<'totp' | 'backup'>('totp');
|
||||||
@ -97,7 +94,7 @@ export const DisableAuthenticatorAppDialog = () => {
|
|||||||
onCloseTwoFactorDisableDialog();
|
onCloseTwoFactorDisableDialog();
|
||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
await revalidate();
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Unable to disable two-factor authentication`),
|
title: _(msg`Unable to disable two-factor authentication`),
|
||||||
@ -1,13 +1,11 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
import { renderSVG } from 'uqr';
|
import { renderSVG } from 'uqr';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@ -50,8 +48,7 @@ export type EnableAuthenticatorAppDialogProps = {
|
|||||||
export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorAppDialogProps) => {
|
export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorAppDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [recoveryCodes, setRecoveryCodes] = useState<string[] | null>(null);
|
const [recoveryCodes, setRecoveryCodes] = useState<string[] | null>(null);
|
||||||
@ -133,7 +130,7 @@ export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorA
|
|||||||
|
|
||||||
if (!isOpen && recoveryCodes && recoveryCodes.length > 0) {
|
if (!isOpen && recoveryCodes && recoveryCodes.length > 0) {
|
||||||
setRecoveryCodes(null);
|
setRecoveryCodes(null);
|
||||||
router.refresh();
|
void revalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Copy } from 'lucide-react';
|
import { Copy } from 'lucide-react';
|
||||||
|
|
||||||
@ -1,16 +1,13 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -147,7 +144,7 @@ export const ViewRecoveryCodesDialog = () => {
|
|||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{match(AppError.parseError(error).message)
|
{match(AppError.parseError(error).message)
|
||||||
.with(ErrorCode.INCORRECT_TWO_FACTOR_CODE, () => (
|
.with('INCORRECT_TWO_FACTOR_CODE', () => (
|
||||||
<Trans>Invalid code. Please try again.</Trans>
|
<Trans>Invalid code. Please try again.</Trans>
|
||||||
))
|
))
|
||||||
.otherwise(() => (
|
.otherwise(() => (
|
||||||
@ -1,22 +1,20 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { ErrorCode, useDropzone } from 'react-dropzone';
|
import { ErrorCode, useDropzone } from 'react-dropzone';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { base64 } from '@documenso/lib/universal/base64';
|
import { base64 } from '@documenso/lib/universal/base64';
|
||||||
|
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { Team, User } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||||
@ -31,6 +29,8 @@ import {
|
|||||||
} from '@documenso/ui/primitives/form/form';
|
} from '@documenso/ui/primitives/form/form';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
export const ZAvatarImageFormSchema = z.object({
|
export const ZAvatarImageFormSchema = z.object({
|
||||||
bytes: z.string().nullish(),
|
bytes: z.string().nullish(),
|
||||||
});
|
});
|
||||||
@ -39,15 +39,15 @@ export type TAvatarImageFormSchema = z.infer<typeof ZAvatarImageFormSchema>;
|
|||||||
|
|
||||||
export type AvatarImageFormProps = {
|
export type AvatarImageFormProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
user: User;
|
|
||||||
team?: Team;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AvatarImageForm = ({ className, user, team }: AvatarImageFormProps) => {
|
export const AvatarImageForm = ({ className }: AvatarImageFormProps) => {
|
||||||
|
const { user } = useSession();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const router = useRouter();
|
const team = useOptionalCurrentTeam();
|
||||||
|
|
||||||
const { mutateAsync: setProfileImage } = trpc.profile.setProfileImage.useMutation();
|
const { mutateAsync: setProfileImage } = trpc.profile.setProfileImage.useMutation();
|
||||||
|
|
||||||
@ -109,7 +109,7 @@ export const AvatarImageForm = ({ className, user, team }: AvatarImageFormProps)
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
void revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -146,11 +146,7 @@ export const AvatarImageForm = ({ className, user, team }: AvatarImageFormProps)
|
|||||||
<div className="flex items-center gap-8">
|
<div className="flex items-center gap-8">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Avatar className="h-16 w-16 border-2 border-solid">
|
<Avatar className="h-16 w-16 border-2 border-solid">
|
||||||
{avatarImageId && (
|
{avatarImageId && <AvatarImage src={formatAvatarUrl(avatarImageId)} />}
|
||||||
<AvatarImage
|
|
||||||
src={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${avatarImageId}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<AvatarFallback className="text-sm text-gray-400">
|
<AvatarFallback className="text-sm text-gray-400">
|
||||||
{initials}
|
{initials}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
@ -1,14 +1,12 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -36,7 +34,7 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
|
|||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const form = useForm<TForgotPasswordFormSchema>({
|
const form = useForm<TForgotPasswordFormSchema>({
|
||||||
values: {
|
values: {
|
||||||
@ -47,10 +45,10 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
|
|||||||
|
|
||||||
const isSubmitting = form.formState.isSubmitting;
|
const isSubmitting = form.formState.isSubmitting;
|
||||||
|
|
||||||
const { mutateAsync: forgotPassword } = trpc.profile.forgotPassword.useMutation();
|
|
||||||
|
|
||||||
const onFormSubmit = async ({ email }: TForgotPasswordFormSchema) => {
|
const onFormSubmit = async ({ email }: TForgotPasswordFormSchema) => {
|
||||||
await forgotPassword({ email }).catch(() => null);
|
await authClient.emailPassword.forgotPassword({ email }).catch(() => null);
|
||||||
|
|
||||||
|
await navigate('/check-email');
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Reset email sent`),
|
title: _(msg`Reset email sent`),
|
||||||
@ -61,8 +59,6 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
form.reset();
|
form.reset();
|
||||||
|
|
||||||
router.push('/check-email');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -1,15 +1,14 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { authClient } from '@documenso/auth/client';
|
||||||
|
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import type { User } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { ZCurrentPasswordSchema, ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
import { ZCurrentPasswordSchema, ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -39,7 +38,7 @@ export type TPasswordFormSchema = z.infer<typeof ZPasswordFormSchema>;
|
|||||||
|
|
||||||
export type PasswordFormProps = {
|
export type PasswordFormProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
user: User;
|
user: SessionUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PasswordForm = ({ className }: PasswordFormProps) => {
|
export const PasswordForm = ({ className }: PasswordFormProps) => {
|
||||||
@ -57,11 +56,9 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
|
|||||||
|
|
||||||
const isSubmitting = form.formState.isSubmitting;
|
const isSubmitting = form.formState.isSubmitting;
|
||||||
|
|
||||||
const { mutateAsync: updatePassword } = trpc.profile.updatePassword.useMutation();
|
|
||||||
|
|
||||||
const onFormSubmit = async ({ currentPassword, password }: TPasswordFormSchema) => {
|
const onFormSubmit = async ({ currentPassword, password }: TPasswordFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await updatePassword({
|
await authClient.emailPassword.updatePassword({
|
||||||
currentPassword,
|
currentPassword,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
@ -1,14 +1,12 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { User } from '@documenso/prisma/client';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -39,14 +37,13 @@ export type TProfileFormSchema = z.infer<typeof ZProfileFormSchema>;
|
|||||||
|
|
||||||
export type ProfileFormProps = {
|
export type ProfileFormProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
user: User;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
export const ProfileForm = ({ className }: ProfileFormProps) => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { user } = useSession();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const form = useForm<TProfileFormSchema>({
|
const form = useForm<TProfileFormSchema>({
|
||||||
values: {
|
values: {
|
||||||
@ -73,7 +70,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
@ -1,19 +1,15 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import type { User } from '@prisma/client';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import profileClaimTeaserImage from '@documenso/assets/images/profile-claim-teaser.png';
|
import profileClaimTeaserImage from '@documenso/assets/images/profile-claim-teaser.png';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { User } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -35,7 +31,7 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { UserProfileSkeleton } from '../ui/user-profile-skeleton';
|
import { UserProfileSkeleton } from '../general/user-profile-skeleton';
|
||||||
|
|
||||||
export const ZClaimPublicProfileFormSchema = z.object({
|
export const ZClaimPublicProfileFormSchema = z.object({
|
||||||
url: z
|
url: z
|
||||||
@ -92,12 +88,12 @@ export const ClaimPublicProfileDialogForm = ({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
if (error.code === AppErrorCode.PROFILE_URL_TAKEN) {
|
if (error.code === 'PROFILE_URL_TAKEN') {
|
||||||
form.setError('url', {
|
form.setError('url', {
|
||||||
type: 'manual',
|
type: 'manual',
|
||||||
message: _(msg`This username is already taken`),
|
message: _(msg`This username is already taken`),
|
||||||
});
|
});
|
||||||
} else if (error.code === AppErrorCode.PREMIUM_PROFILE_URL) {
|
} else if (error.code === 'PREMIUM_PROFILE_URL') {
|
||||||
form.setError('url', {
|
form.setError('url', {
|
||||||
type: 'manual',
|
type: 'manual',
|
||||||
message: error.message,
|
message: error.message,
|
||||||
@ -135,7 +131,7 @@ export const ClaimPublicProfileDialogForm = ({
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<Image src={profileClaimTeaserImage} alt="profile claim teaser" />
|
<img src={profileClaimTeaserImage} alt="profile claim teaser" />
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@ -1,10 +1,10 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Plural, Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Plural, Trans } from '@lingui/react/macro';
|
||||||
|
import type { TeamProfile, UserProfile } from '@prisma/client';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import { CheckSquareIcon, CopyIcon } from 'lucide-react';
|
import { CheckSquareIcon, CopyIcon } from 'lucide-react';
|
||||||
@ -12,9 +12,8 @@ import { useForm } from 'react-hook-form';
|
|||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { formatUserProfilePath } from '@documenso/lib/utils/public-profiles';
|
import { formatUserProfilePath } from '@documenso/lib/utils/public-profiles';
|
||||||
import type { TeamProfile, UserProfile } from '@documenso/prisma/client';
|
|
||||||
import {
|
import {
|
||||||
MAX_PROFILE_BIO_LENGTH,
|
MAX_PROFILE_BIO_LENGTH,
|
||||||
ZUpdatePublicProfileMutationSchema,
|
ZUpdatePublicProfileMutationSchema,
|
||||||
@ -90,8 +89,8 @@ export const PublicProfileForm = ({
|
|||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
switch (error.code) {
|
switch (error.code) {
|
||||||
case AppErrorCode.PREMIUM_PROFILE_URL:
|
case 'PREMIUM_PROFILE_URL':
|
||||||
case AppErrorCode.PROFILE_URL_TAKEN:
|
case 'PROFILE_URL_TAKEN':
|
||||||
form.setError('url', {
|
form.setError('url', {
|
||||||
type: 'manual',
|
type: 'manual',
|
||||||
message: error.message,
|
message: error.message,
|
||||||
@ -1,16 +1,14 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -43,7 +41,7 @@ export type ResetPasswordFormProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps) => {
|
export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps) => {
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@ -58,15 +56,15 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps)
|
|||||||
|
|
||||||
const isSubmitting = form.formState.isSubmitting;
|
const isSubmitting = form.formState.isSubmitting;
|
||||||
|
|
||||||
const { mutateAsync: resetPassword } = trpc.profile.resetPassword.useMutation();
|
|
||||||
|
|
||||||
const onFormSubmit = async ({ password }: Omit<TResetPasswordFormSchema, 'repeatedPassword'>) => {
|
const onFormSubmit = async ({ password }: Omit<TResetPasswordFormSchema, 'repeatedPassword'>) => {
|
||||||
try {
|
try {
|
||||||
await resetPassword({
|
await authClient.emailPassword.resetPassword({
|
||||||
password,
|
password,
|
||||||
token,
|
token,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await navigate('/signin');
|
||||||
|
|
||||||
form.reset();
|
form.reset();
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@ -74,8 +72,6 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps)
|
|||||||
description: _(msg`Your password has been updated successfully.`),
|
description: _(msg`Your password has been updated successfully.`),
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push('/signin');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
import { useLocation, useNavigate } from 'react-router';
|
||||||
|
import { useSearchParams } from 'react-router';
|
||||||
|
|
||||||
import { Select, SelectContent, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
|
import { Select, SelectContent, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
|
||||||
|
|
||||||
@ -11,10 +12,10 @@ export type SearchParamSelector = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SearchParamSelector = ({ children, paramKey, isValueValid }: SearchParamSelector) => {
|
export const SearchParamSelector = ({ children, paramKey, isValueValid }: SearchParamSelector) => {
|
||||||
const pathname = usePathname();
|
const { pathname } = useLocation();
|
||||||
const searchParams = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const value = useMemo(() => {
|
const value = useMemo(() => {
|
||||||
const p = searchParams?.get(paramKey) ?? 'all';
|
const p = searchParams?.get(paramKey) ?? 'all';
|
||||||
@ -35,7 +36,7 @@ export const SearchParamSelector = ({ children, paramKey, isValueValid }: Search
|
|||||||
params.delete(paramKey);
|
params.delete(paramKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
router.push(`${pathname}?${params.toString()}`, { scroll: false });
|
void navigate(`${pathname}?${params.toString()}`, { preventScrollReset: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -1,12 +1,11 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -43,11 +42,9 @@ export const SendConfirmationEmailForm = ({ className }: SendConfirmationEmailFo
|
|||||||
|
|
||||||
const isSubmitting = form.formState.isSubmitting;
|
const isSubmitting = form.formState.isSubmitting;
|
||||||
|
|
||||||
const { mutateAsync: sendConfirmationEmail } = trpc.profile.sendConfirmationEmail.useMutation();
|
|
||||||
|
|
||||||
const onFormSubmit = async ({ email }: TSendConfirmationEmailFormSchema) => {
|
const onFormSubmit = async ({ email }: TSendConfirmationEmailFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await sendConfirmationEmail({ email });
|
await authClient.emailPassword.resendVerifyEmail({ email });
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Confirmation email sent`),
|
title: _(msg`Confirmation email sent`),
|
||||||
@ -60,6 +57,7 @@ export const SendConfirmationEmailForm = ({ className }: SendConfirmationEmailFo
|
|||||||
form.reset();
|
form.reset();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
|
variant: 'destructive',
|
||||||
title: _(msg`An error occurred while sending your confirmation email`),
|
title: _(msg`An error occurred while sending your confirmation email`),
|
||||||
description: _(msg`Please try again and make sure you enter the correct email address.`),
|
description: _(msg`Please try again and make sure you enter the correct email address.`),
|
||||||
});
|
});
|
||||||
@ -1,25 +1,22 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import type { MessageDescriptor } from '@lingui/core';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser';
|
import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser';
|
||||||
import { KeyRoundIcon } from 'lucide-react';
|
import { KeyRoundIcon } from 'lucide-react';
|
||||||
import { signIn } from 'next-auth/react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { FaIdCardClip } from 'react-icons/fa6';
|
import { FaIdCardClip } from 'react-icons/fa6';
|
||||||
import { FcGoogle } from 'react-icons/fc';
|
import { FcGoogle } from 'react-icons/fc';
|
||||||
|
import { Link, useNavigate } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AuthenticationErrorCode } from '@documenso/auth/server/lib/errors/error-codes';
|
||||||
import { ErrorCode, isErrorCode } from '@documenso/lib/next-auth/error-codes';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { ZCurrentPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
import { ZCurrentPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@ -44,19 +41,19 @@ import { PasswordInput } from '@documenso/ui/primitives/password-input';
|
|||||||
import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input';
|
import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
const ERROR_MESSAGES: Partial<Record<keyof typeof ErrorCode, string>> = {
|
const CommonErrorMessages: Record<string, MessageDescriptor> = {
|
||||||
[ErrorCode.CREDENTIALS_NOT_FOUND]: 'The email or password provided is incorrect',
|
[AuthenticationErrorCode.AccountDisabled]: msg`This account has been disabled. Please contact support.`,
|
||||||
[ErrorCode.INCORRECT_EMAIL_PASSWORD]: 'The email or password provided is incorrect',
|
|
||||||
[ErrorCode.USER_MISSING_PASSWORD]:
|
|
||||||
'This account appears to be using a social login method, please sign in using that method',
|
|
||||||
[ErrorCode.INCORRECT_TWO_FACTOR_CODE]: 'The two-factor authentication code provided is incorrect',
|
|
||||||
[ErrorCode.INCORRECT_TWO_FACTOR_BACKUP_CODE]: 'The backup code provided is incorrect',
|
|
||||||
[ErrorCode.UNVERIFIED_EMAIL]:
|
|
||||||
'This account has not been verified. Please verify your account before signing in.',
|
|
||||||
[ErrorCode.ACCOUNT_DISABLED]: 'This account has been disabled. Please contact support.',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const TwoFactorEnabledErrorCode = ErrorCode.TWO_FACTOR_MISSING_CREDENTIALS;
|
const handleFallbackErrorMessages = (code: string) => {
|
||||||
|
const message = CommonErrorMessages[code];
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return msg`An unknown error occurred`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
};
|
||||||
|
|
||||||
const LOGIN_REDIRECT_PATH = '/documents';
|
const LOGIN_REDIRECT_PATH = '/documents';
|
||||||
|
|
||||||
@ -88,9 +85,8 @@ export const SignInForm = ({
|
|||||||
}: SignInFormProps) => {
|
}: SignInFormProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { getFlag } = useFeatureFlags();
|
|
||||||
|
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] =
|
const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
@ -101,9 +97,7 @@ export const SignInForm = ({
|
|||||||
|
|
||||||
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
|
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
|
||||||
|
|
||||||
const isPasskeyEnabled = getFlag('app_passkey');
|
const redirectPath = useMemo(() => {
|
||||||
|
|
||||||
const callbackUrl = useMemo(() => {
|
|
||||||
// Handle SSR
|
// Handle SSR
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return LOGIN_REDIRECT_PATH;
|
return LOGIN_REDIRECT_PATH;
|
||||||
@ -170,25 +164,20 @@ export const SignInForm = ({
|
|||||||
try {
|
try {
|
||||||
setIsPasskeyLoading(true);
|
setIsPasskeyLoading(true);
|
||||||
|
|
||||||
const options = await createPasskeySigninOptions();
|
const { options, sessionId } = await createPasskeySigninOptions();
|
||||||
|
|
||||||
const credential = await startAuthentication(options);
|
const credential = await startAuthentication(options);
|
||||||
|
|
||||||
const result = await signIn('webauthn', {
|
await authClient.passkey.signIn({
|
||||||
credential: JSON.stringify(credential),
|
credential: JSON.stringify(credential),
|
||||||
callbackUrl,
|
csrfToken: sessionId,
|
||||||
redirect: false,
|
redirectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result?.url || result.error) {
|
|
||||||
throw new AppError(result?.error ?? '');
|
|
||||||
}
|
|
||||||
|
|
||||||
window.location.href = result.url;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setIsPasskeyLoading(false);
|
setIsPasskeyLoading(false);
|
||||||
|
|
||||||
if (err.name === 'NotAllowedError') {
|
// Error from library.
|
||||||
|
if (err instanceof Error && err.name === 'NotAllowedError') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,12 +185,15 @@ export const SignInForm = ({
|
|||||||
|
|
||||||
const errorMessage = match(error.code)
|
const errorMessage = match(error.code)
|
||||||
.with(
|
.with(
|
||||||
AppErrorCode.NOT_SETUP,
|
AuthenticationErrorCode.NotSetup,
|
||||||
() =>
|
() =>
|
||||||
msg`This passkey is not configured for this application. Please login and add one in the user settings.`,
|
msg`This passkey is not configured for this application. Please login and add one in the user settings.`,
|
||||||
)
|
)
|
||||||
.with(AppErrorCode.EXPIRED_CODE, () => msg`This session has expired. Please try again.`)
|
.with(
|
||||||
.otherwise(() => msg`Please try again later or login using your normal details`);
|
AuthenticationErrorCode.SessionExpired,
|
||||||
|
() => msg`This session has expired. Please try again.`,
|
||||||
|
)
|
||||||
|
.otherwise(() => handleFallbackErrorMessages(error.code));
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: 'Something went wrong',
|
title: 'Something went wrong',
|
||||||
@ -214,73 +206,58 @@ export const SignInForm = ({
|
|||||||
|
|
||||||
const onFormSubmit = async ({ email, password, totpCode, backupCode }: TSignInFormSchema) => {
|
const onFormSubmit = async ({ email, password, totpCode, backupCode }: TSignInFormSchema) => {
|
||||||
try {
|
try {
|
||||||
const credentials: Record<string, string> = {
|
await authClient.emailPassword.signIn({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
};
|
totpCode,
|
||||||
|
backupCode,
|
||||||
if (totpCode) {
|
redirectPath,
|
||||||
credentials.totpCode = totpCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (backupCode) {
|
|
||||||
credentials.backupCode = backupCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await signIn('credentials', {
|
|
||||||
...credentials,
|
|
||||||
callbackUrl,
|
|
||||||
redirect: false,
|
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
|
||||||
if (result?.error && isErrorCode(result.error)) {
|
const error = AppError.parseError(err);
|
||||||
if (result.error === TwoFactorEnabledErrorCode) {
|
|
||||||
setIsTwoFactorAuthenticationDialogOpen(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorMessage = ERROR_MESSAGES[result.error];
|
if (error.code === 'TWO_FACTOR_MISSING_CREDENTIALS') {
|
||||||
|
setIsTwoFactorAuthenticationDialogOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (result.error === ErrorCode.UNVERIFIED_EMAIL) {
|
if (error.code === AuthenticationErrorCode.UnverifiedEmail) {
|
||||||
router.push(`/unverified-account`);
|
await navigate('/unverified-account');
|
||||||
|
|
||||||
toast({
|
|
||||||
title: _(msg`Unable to sign in`),
|
|
||||||
description: errorMessage ?? _(msg`An unknown error occurred`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Unable to sign in`),
|
title: _(msg`Unable to sign in`),
|
||||||
description: errorMessage ?? _(msg`An unknown error occurred`),
|
description: _(
|
||||||
variant: 'destructive',
|
msg`This account has not been verified. Please verify your account before signing in.`,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result?.url) {
|
const errorMessage = match(error.code)
|
||||||
throw new Error('An unknown error occurred');
|
.with(
|
||||||
}
|
AuthenticationErrorCode.InvalidCredentials,
|
||||||
|
() => msg`The email or password provided is incorrect`,
|
||||||
|
)
|
||||||
|
.with(
|
||||||
|
AuthenticationErrorCode.InvalidTwoFactorCode,
|
||||||
|
() => msg`The two-factor authentication code provided is incorrect`,
|
||||||
|
)
|
||||||
|
.otherwise(() => handleFallbackErrorMessages(error.code));
|
||||||
|
|
||||||
window.location.href = result.url;
|
|
||||||
} catch (err) {
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`Unable to sign in`),
|
||||||
description: _(
|
description: _(errorMessage),
|
||||||
msg`We encountered an unknown error while attempting to sign you In. Please try again later.`,
|
variant: 'destructive',
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSignInWithGoogleClick = async () => {
|
const onSignInWithGoogleClick = async () => {
|
||||||
try {
|
try {
|
||||||
await signIn('google', {
|
await authClient.google.signIn();
|
||||||
callbackUrl,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
@ -294,9 +271,11 @@ export const SignInForm = ({
|
|||||||
|
|
||||||
const onSignInWithOIDCClick = async () => {
|
const onSignInWithOIDCClick = async () => {
|
||||||
try {
|
try {
|
||||||
await signIn('oidc', {
|
// eslint-disable-next-line no-promise-executor-return
|
||||||
callbackUrl,
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
});
|
// await signIn('oidc', {
|
||||||
|
// callbackUrl,
|
||||||
|
// });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
@ -365,7 +344,7 @@ export const SignInForm = ({
|
|||||||
|
|
||||||
<p className="mt-2 text-right">
|
<p className="mt-2 text-right">
|
||||||
<Link
|
<Link
|
||||||
href="/forgot-password"
|
to="/forgot-password"
|
||||||
className="text-muted-foreground text-sm duration-200 hover:opacity-70"
|
className="text-muted-foreground text-sm duration-200 hover:opacity-70"
|
||||||
>
|
>
|
||||||
<Trans>Forgot your password?</Trans>
|
<Trans>Forgot your password?</Trans>
|
||||||
@ -384,7 +363,7 @@ export const SignInForm = ({
|
|||||||
{isSubmitting ? <Trans>Signing in...</Trans> : <Trans>Sign In</Trans>}
|
{isSubmitting ? <Trans>Signing in...</Trans> : <Trans>Sign In</Trans>}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{(isGoogleSSOEnabled || isPasskeyEnabled || isOIDCSSOEnabled) && (
|
{(isGoogleSSOEnabled || isOIDCSSOEnabled) && (
|
||||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||||
<div className="bg-border h-px flex-1" />
|
<div className="bg-border h-px flex-1" />
|
||||||
<span className="text-muted-foreground bg-transparent">
|
<span className="text-muted-foreground bg-transparent">
|
||||||
@ -422,20 +401,18 @@ export const SignInForm = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isPasskeyEnabled && (
|
<Button
|
||||||
<Button
|
type="button"
|
||||||
type="button"
|
size="lg"
|
||||||
size="lg"
|
variant="outline"
|
||||||
variant="outline"
|
disabled={isSubmitting}
|
||||||
disabled={isSubmitting}
|
loading={isPasskeyLoading}
|
||||||
loading={isPasskeyLoading}
|
className="bg-background text-muted-foreground border"
|
||||||
className="bg-background text-muted-foreground border"
|
onClick={onSignInWithPasskey}
|
||||||
onClick={onSignInWithPasskey}
|
>
|
||||||
>
|
{!isPasskeyLoading && <KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />}
|
||||||
{!isPasskeyLoading && <KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />}
|
<Trans>Passkey</Trans>
|
||||||
<Trans>Passkey</Trans>
|
</Button>
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -1,27 +1,22 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import type { MessageDescriptor } from '@lingui/core';
|
import type { MessageDescriptor } from '@lingui/core';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { signIn } from 'next-auth/react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { FaIdCardClip } from 'react-icons/fa6';
|
import { FaIdCardClip } from 'react-icons/fa6';
|
||||||
import { FcGoogle } from 'react-icons/fc';
|
import { FcGoogle } from 'react-icons/fc';
|
||||||
|
import { Link, useNavigate, useSearchParams } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import communityCardsImage from '@documenso/assets/images/community-cards.png';
|
import communityCardsImage from '@documenso/assets/images/community-cards.png';
|
||||||
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -38,14 +33,14 @@ import { PasswordInput } from '@documenso/ui/primitives/password-input';
|
|||||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { UserProfileSkeleton } from '~/components/ui/user-profile-skeleton';
|
import { UserProfileSkeleton } from '~/components/general/user-profile-skeleton';
|
||||||
import { UserProfileTimur } from '~/components/ui/user-profile-timur';
|
import { UserProfileTimur } from '~/components/general/user-profile-timur';
|
||||||
|
|
||||||
const SIGN_UP_REDIRECT_PATH = '/documents';
|
const SIGN_UP_REDIRECT_PATH = '/documents';
|
||||||
|
|
||||||
type SignUpStep = 'BASIC_DETAILS' | 'CLAIM_USERNAME';
|
type SignUpStep = 'BASIC_DETAILS' | 'CLAIM_USERNAME';
|
||||||
|
|
||||||
export const ZSignUpFormV2Schema = z
|
export const ZSignUpFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
@ -78,39 +73,39 @@ export const signupErrorMessages: Record<string, MessageDescriptor> = {
|
|||||||
SIGNUP_DISABLED: msg`Signups are disabled.`,
|
SIGNUP_DISABLED: msg`Signups are disabled.`,
|
||||||
[AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`,
|
[AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`,
|
||||||
[AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`,
|
[AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`,
|
||||||
[AppErrorCode.PROFILE_URL_TAKEN]: msg`This username has already been taken`,
|
PROFILE_URL_TAKEN: msg`This username has already been taken`,
|
||||||
[AppErrorCode.PREMIUM_PROFILE_URL]: msg`Only subscribers can have a username shorter than 6 characters`,
|
PREMIUM_PROFILE_URL: msg`Only subscribers can have a username shorter than 6 characters`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSignUpFormV2Schema = z.infer<typeof ZSignUpFormV2Schema>;
|
export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
|
||||||
|
|
||||||
export type SignUpFormV2Props = {
|
export type SignUpFormProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
initialEmail?: string;
|
initialEmail?: string;
|
||||||
isGoogleSSOEnabled?: boolean;
|
isGoogleSSOEnabled?: boolean;
|
||||||
isOIDCSSOEnabled?: boolean;
|
isOIDCSSOEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SignUpFormV2 = ({
|
export const SignUpForm = ({
|
||||||
className,
|
className,
|
||||||
initialEmail,
|
initialEmail,
|
||||||
isGoogleSSOEnabled,
|
isGoogleSSOEnabled,
|
||||||
isOIDCSSOEnabled,
|
isOIDCSSOEnabled,
|
||||||
}: SignUpFormV2Props) => {
|
}: SignUpFormProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const analytics = useAnalytics();
|
const analytics = useAnalytics();
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const searchParams = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const [step, setStep] = useState<SignUpStep>('BASIC_DETAILS');
|
const [step, setStep] = useState<SignUpStep>('BASIC_DETAILS');
|
||||||
|
|
||||||
const utmSrc = searchParams?.get('utm_source') ?? null;
|
const utmSrc = searchParams.get('utm_source') ?? null;
|
||||||
|
|
||||||
const baseUrl = new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000');
|
const baseUrl = new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000');
|
||||||
|
|
||||||
const form = useForm<TSignUpFormV2Schema>({
|
const form = useForm<TSignUpFormSchema>({
|
||||||
values: {
|
values: {
|
||||||
name: '',
|
name: '',
|
||||||
email: initialEmail ?? '',
|
email: initialEmail ?? '',
|
||||||
@ -119,7 +114,7 @@ export const SignUpFormV2 = ({
|
|||||||
url: '',
|
url: '',
|
||||||
},
|
},
|
||||||
mode: 'onBlur',
|
mode: 'onBlur',
|
||||||
resolver: zodResolver(ZSignUpFormV2Schema),
|
resolver: zodResolver(ZSignUpFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const isSubmitting = form.formState.isSubmitting;
|
const isSubmitting = form.formState.isSubmitting;
|
||||||
@ -127,13 +122,17 @@ export const SignUpFormV2 = ({
|
|||||||
const name = form.watch('name');
|
const name = form.watch('name');
|
||||||
const url = form.watch('url');
|
const url = form.watch('url');
|
||||||
|
|
||||||
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
|
const onFormSubmit = async ({ name, email, password, signature, url }: TSignUpFormSchema) => {
|
||||||
|
|
||||||
const onFormSubmit = async ({ name, email, password, signature, url }: TSignUpFormV2Schema) => {
|
|
||||||
try {
|
try {
|
||||||
await signup({ name, email, password, signature, url });
|
await authClient.emailPassword.signUp({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
signature,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
|
||||||
router.push(`/unverified-account`);
|
await navigate(`/unverified-account`);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Registration Successful`),
|
title: _(msg`Registration Successful`),
|
||||||
@ -153,10 +152,7 @@ export const SignUpFormV2 = ({
|
|||||||
|
|
||||||
const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST;
|
const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST;
|
||||||
|
|
||||||
if (
|
if (error.code === 'PROFILE_URL_TAKEN' || error.code === 'PREMIUM_PROFILE_URL') {
|
||||||
error.code === AppErrorCode.PROFILE_URL_TAKEN ||
|
|
||||||
error.code === AppErrorCode.PREMIUM_PROFILE_URL
|
|
||||||
) {
|
|
||||||
form.setError('url', {
|
form.setError('url', {
|
||||||
type: 'manual',
|
type: 'manual',
|
||||||
message: _(errorMessage),
|
message: _(errorMessage),
|
||||||
@ -181,7 +177,7 @@ export const SignUpFormV2 = ({
|
|||||||
|
|
||||||
const onSignUpWithGoogleClick = async () => {
|
const onSignUpWithGoogleClick = async () => {
|
||||||
try {
|
try {
|
||||||
await signIn('google', { callbackUrl: SIGN_UP_REDIRECT_PATH });
|
await authClient.google.signIn();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
@ -195,7 +191,9 @@ export const SignUpFormV2 = ({
|
|||||||
|
|
||||||
const onSignUpWithOIDCClick = async () => {
|
const onSignUpWithOIDCClick = async () => {
|
||||||
try {
|
try {
|
||||||
await signIn('oidc', { callbackUrl: SIGN_UP_REDIRECT_PATH });
|
// eslint-disable-next-line no-promise-executor-return
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
// await signIn('oidc', { callbackUrl: SIGN_UP_REDIRECT_PATH });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
@ -223,11 +221,10 @@ export const SignUpFormV2 = ({
|
|||||||
<div className={cn('flex justify-center gap-x-12', className)}>
|
<div className={cn('flex justify-center gap-x-12', className)}>
|
||||||
<div className="border-border relative hidden flex-1 overflow-hidden rounded-xl border xl:flex">
|
<div className="border-border relative hidden flex-1 overflow-hidden rounded-xl border xl:flex">
|
||||||
<div className="absolute -inset-8 -z-[2] backdrop-blur">
|
<div className="absolute -inset-8 -z-[2] backdrop-blur">
|
||||||
<Image
|
<img
|
||||||
src={communityCardsImage}
|
src={communityCardsImage}
|
||||||
fill={true}
|
|
||||||
alt="community-cards"
|
alt="community-cards"
|
||||||
className="dark:brightness-95 dark:contrast-[70%] dark:invert"
|
className="h-full w-full object-cover dark:brightness-95 dark:contrast-[70%] dark:invert"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -426,10 +423,7 @@ export const SignUpFormV2 = ({
|
|||||||
<p className="text-muted-foreground mt-4 text-sm">
|
<p className="text-muted-foreground mt-4 text-sm">
|
||||||
<Trans>
|
<Trans>
|
||||||
Already have an account?{' '}
|
Already have an account?{' '}
|
||||||
<Link
|
<Link to="/signin" className="text-documenso-700 duration-200 hover:opacity-70">
|
||||||
href="/signin"
|
|
||||||
className="text-documenso-700 duration-200 hover:opacity-70"
|
|
||||||
>
|
|
||||||
Sign in instead
|
Sign in instead
|
||||||
</Link>
|
</Link>
|
||||||
</Trans>
|
</Trans>
|
||||||
@ -1,17 +1,16 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Team, TeamGlobalSettings } from '@prisma/client';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
||||||
import { putFile } from '@documenso/lib/universal/upload/put-file';
|
import { putFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
import type { Team, TeamGlobalSettings } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -1,19 +1,18 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Team, TeamGlobalSettings } from '@prisma/client';
|
||||||
|
import { DocumentVisibility } from '@prisma/client';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import {
|
import {
|
||||||
SUPPORTED_LANGUAGES,
|
SUPPORTED_LANGUAGES,
|
||||||
SUPPORTED_LANGUAGE_CODES,
|
SUPPORTED_LANGUAGE_CODES,
|
||||||
isValidLanguageCode,
|
isValidLanguageCode,
|
||||||
} from '@documenso/lib/constants/i18n';
|
} from '@documenso/lib/constants/i18n';
|
||||||
import type { Team, TeamGlobalSettings } from '@documenso/prisma/client';
|
|
||||||
import { DocumentVisibility } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert } from '@documenso/ui/primitives/alert';
|
import { Alert } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -56,9 +55,9 @@ export const TeamDocumentPreferencesForm = ({
|
|||||||
}: TeamDocumentPreferencesFormProps) => {
|
}: TeamDocumentPreferencesFormProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { data } = useSession();
|
const { user } = useSession();
|
||||||
|
|
||||||
const placeholderEmail = data?.user.email ?? 'user@example.com';
|
const placeholderEmail = user.email ?? 'user@example.com';
|
||||||
|
|
||||||
const { mutateAsync: updateTeamDocumentPreferences } =
|
const { mutateAsync: updateTeamDocumentPreferences } =
|
||||||
trpc.team.updateTeamDocumentSettings.useMutation();
|
trpc.team.updateTeamDocumentSettings.useMutation();
|
||||||
@ -1,15 +1,13 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { ZUpdateTeamMutationSchema } from '@documenso/trpc/server/team-router/schema';
|
import { ZUpdateTeamMutationSchema } from '@documenso/trpc/server/team-router/schema';
|
||||||
@ -31,20 +29,20 @@ export type UpdateTeamDialogProps = {
|
|||||||
teamUrl: string;
|
teamUrl: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ZUpdateTeamFormSchema = ZUpdateTeamMutationSchema.shape.data.pick({
|
const ZTeamUpdateFormSchema = ZUpdateTeamMutationSchema.shape.data.pick({
|
||||||
name: true,
|
name: true,
|
||||||
url: true,
|
url: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
type TUpdateTeamFormSchema = z.infer<typeof ZUpdateTeamFormSchema>;
|
type TTeamUpdateFormSchema = z.infer<typeof ZTeamUpdateFormSchema>;
|
||||||
|
|
||||||
export const UpdateTeamForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogProps) => {
|
export const TeamUpdateForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogProps) => {
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(ZUpdateTeamFormSchema),
|
resolver: zodResolver(ZTeamUpdateFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: teamName,
|
name: teamName,
|
||||||
url: teamUrl,
|
url: teamUrl,
|
||||||
@ -53,7 +51,7 @@ export const UpdateTeamForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogPr
|
|||||||
|
|
||||||
const { mutateAsync: updateTeam } = trpc.team.updateTeam.useMutation();
|
const { mutateAsync: updateTeam } = trpc.team.updateTeam.useMutation();
|
||||||
|
|
||||||
const onFormSubmit = async ({ name, url }: TUpdateTeamFormSchema) => {
|
const onFormSubmit = async ({ name, url }: TTeamUpdateFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await updateTeam({
|
await updateTeam({
|
||||||
data: {
|
data: {
|
||||||
@ -75,7 +73,7 @@ export const UpdateTeamForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogPr
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (url !== teamUrl) {
|
if (url !== teamUrl) {
|
||||||
router.push(`${WEBAPP_BASE_URL}/t/${url}/settings`);
|
await navigate(`/t/${url}/settings`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
@ -133,7 +131,7 @@ export const UpdateTeamForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogPr
|
|||||||
{!form.formState.errors.url && (
|
{!form.formState.errors.url && (
|
||||||
<span className="text-foreground/50 text-xs font-normal">
|
<span className="text-foreground/50 text-xs font-normal">
|
||||||
{field.value ? (
|
{field.value ? (
|
||||||
`${WEBAPP_BASE_URL}/t/${field.value}`
|
`${NEXT_PUBLIC_WEBAPP_URL()}/t/${field.value}`
|
||||||
) : (
|
) : (
|
||||||
<Trans>A unique URL to identify your team</Trans>
|
<Trans>A unique URL to identify your team</Trans>
|
||||||
)}
|
)}
|
||||||
@ -1,12 +1,10 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useTransition } from 'react';
|
import { useState, useTransition } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { ApiToken } from '@prisma/client';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
@ -14,7 +12,6 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { ApiToken } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type { TCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
|
import type { TCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
|
||||||
import { ZCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
|
import { ZCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
|
||||||
@ -41,7 +38,13 @@ import {
|
|||||||
import { Switch } from '@documenso/ui/primitives/switch';
|
import { Switch } from '@documenso/ui/primitives/switch';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { EXPIRATION_DATES } from '../(dashboard)/settings/token/contants';
|
export const EXPIRATION_DATES = {
|
||||||
|
ONE_WEEK: msg`7 days`,
|
||||||
|
ONE_MONTH: msg`1 month`,
|
||||||
|
THREE_MONTHS: msg`3 months`,
|
||||||
|
SIX_MONTHS: msg`6 months`,
|
||||||
|
ONE_YEAR: msg`12 months`,
|
||||||
|
} as const;
|
||||||
|
|
||||||
const ZCreateTokenFormSchema = ZCreateTokenMutationSchema.extend({
|
const ZCreateTokenFormSchema = ZCreateTokenMutationSchema.extend({
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
@ -61,7 +64,6 @@ export type ApiTokenFormProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) => {
|
export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) => {
|
||||||
const router = useRouter();
|
|
||||||
const [isTransitionPending, startTransition] = useTransition();
|
const [isTransitionPending, startTransition] = useTransition();
|
||||||
|
|
||||||
const [, copy] = useCopyToClipboard();
|
const [, copy] = useCopyToClipboard();
|
||||||
@ -72,13 +74,6 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
|
|||||||
const [newlyCreatedToken, setNewlyCreatedToken] = useState<NewlyCreatedToken | null>();
|
const [newlyCreatedToken, setNewlyCreatedToken] = useState<NewlyCreatedToken | null>();
|
||||||
const [noExpirationDate, setNoExpirationDate] = useState(false);
|
const [noExpirationDate, setNoExpirationDate] = useState(false);
|
||||||
|
|
||||||
// This lets us hide the token from being copied if it has been deleted without
|
|
||||||
// resorting to a useEffect or any other fanciness. This comes at the cost of it
|
|
||||||
// taking slighly longer to appear since it will need to wait for the router.refresh()
|
|
||||||
// to finish updating.
|
|
||||||
const hasNewlyCreatedToken =
|
|
||||||
tokens?.find((token) => token.id === newlyCreatedToken?.id) !== undefined;
|
|
||||||
|
|
||||||
const { mutateAsync: createTokenMutation } = trpc.apiToken.createToken.useMutation({
|
const { mutateAsync: createTokenMutation } = trpc.apiToken.createToken.useMutation({
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
setNewlyCreatedToken(data);
|
setNewlyCreatedToken(data);
|
||||||
@ -130,8 +125,6 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
|
|||||||
});
|
});
|
||||||
|
|
||||||
form.reset();
|
form.reset();
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -263,34 +256,36 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
|
|||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<AnimatePresence initial={!hasNewlyCreatedToken}>
|
<AnimatePresence>
|
||||||
{newlyCreatedToken && hasNewlyCreatedToken && (
|
{newlyCreatedToken &&
|
||||||
<motion.div
|
tokens &&
|
||||||
className="mt-8"
|
tokens.find((token) => token.id === newlyCreatedToken.id) && (
|
||||||
initial={{ opacity: 0, y: -40 }}
|
<motion.div
|
||||||
animate={{ opacity: 1, y: 0 }}
|
className="mt-8"
|
||||||
exit={{ opacity: 0, y: 40 }}
|
initial={{ opacity: 0, y: -40 }}
|
||||||
>
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<Card gradient>
|
exit={{ opacity: 0, y: 40 }}
|
||||||
<CardContent className="p-4">
|
>
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
<Card gradient>
|
||||||
<Trans>
|
<CardContent className="p-4">
|
||||||
Your token was created successfully! Make sure to copy it because you won't be
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
able to see it again!
|
<Trans>
|
||||||
</Trans>
|
Your token was created successfully! Make sure to copy it because you won't be
|
||||||
</p>
|
able to see it again!
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p className="bg-muted-foreground/10 my-4 rounded-md px-2.5 py-1 font-mono text-sm">
|
<p className="bg-muted-foreground/10 my-4 rounded-md px-2.5 py-1 font-mono text-sm">
|
||||||
{newlyCreatedToken.token}
|
{newlyCreatedToken.token}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button variant="outline" onClick={() => void copyToken(newlyCreatedToken.token)}>
|
<Button variant="outline" onClick={() => void copyToken(newlyCreatedToken.token)}>
|
||||||
<Trans>Copy token</Trans>
|
<Trans>Copy token</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -1,23 +1,21 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||||
|
|
||||||
import type { GetSignerConversionMonthlyResult } from '@documenso/lib/server-only/user/get-signer-conversion';
|
import type { GetSignerConversionMonthlyResult } from '@documenso/lib/server-only/user/get-signer-conversion';
|
||||||
|
|
||||||
export type SignerConversionChartProps = {
|
export type AdminStatsSignerConversionChartProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
title: string;
|
title: string;
|
||||||
cummulative?: boolean;
|
cummulative?: boolean;
|
||||||
data: GetSignerConversionMonthlyResult;
|
data: GetSignerConversionMonthlyResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SignerConversionChart = ({
|
export const AdminStatsSignerConversionChart = ({
|
||||||
className,
|
className,
|
||||||
data,
|
data,
|
||||||
title,
|
title,
|
||||||
cummulative = false,
|
cummulative = false,
|
||||||
}: SignerConversionChartProps) => {
|
}: AdminStatsSignerConversionChartProps) => {
|
||||||
const formattedData = [...data].reverse().map(({ month, count, cume_count }) => {
|
const formattedData = [...data].reverse().map(({ month, count, cume_count }) => {
|
||||||
return {
|
return {
|
||||||
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('MMM yyyy'),
|
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('MMM yyyy'),
|
||||||
@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||||
import type { TooltipProps } from 'recharts';
|
import type { TooltipProps } from 'recharts';
|
||||||
@ -7,7 +5,7 @@ import type { NameType, ValueType } from 'recharts/types/component/DefaultToolti
|
|||||||
|
|
||||||
import type { GetUserWithDocumentMonthlyGrowth } from '@documenso/lib/server-only/admin/get-users-stats';
|
import type { GetUserWithDocumentMonthlyGrowth } from '@documenso/lib/server-only/admin/get-users-stats';
|
||||||
|
|
||||||
export type UserWithDocumentChartProps = {
|
export type AdminStatsUsersWithDocumentsChartProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
title: string;
|
title: string;
|
||||||
data: GetUserWithDocumentMonthlyGrowth;
|
data: GetUserWithDocumentMonthlyGrowth;
|
||||||
@ -23,7 +21,7 @@ const CustomTooltip = ({
|
|||||||
}: TooltipProps<ValueType, NameType> & { tooltip?: string }) => {
|
}: TooltipProps<ValueType, NameType> & { tooltip?: string }) => {
|
||||||
if (active && payload && payload.length) {
|
if (active && payload && payload.length) {
|
||||||
return (
|
return (
|
||||||
<div className="z-100 w-60 space-y-1 rounded-md border border-solid bg-white p-2 px-3">
|
<div className="z-100 w-60 space-y-1 rounded-md border border-solid bg-white p-2 px-3">
|
||||||
<p className="">{label}</p>
|
<p className="">{label}</p>
|
||||||
<p className="text-documenso">
|
<p className="text-documenso">
|
||||||
{`${tooltip} : `}
|
{`${tooltip} : `}
|
||||||
@ -36,13 +34,13 @@ const CustomTooltip = ({
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserWithDocumentChart = ({
|
export const AdminStatsUsersWithDocumentsChart = ({
|
||||||
className,
|
className,
|
||||||
data,
|
data,
|
||||||
title,
|
title,
|
||||||
completed = false,
|
completed = false,
|
||||||
tooltip,
|
tooltip,
|
||||||
}: UserWithDocumentChartProps) => {
|
}: AdminStatsUsersWithDocumentsChartProps) => {
|
||||||
const formattedData = (data: GetUserWithDocumentMonthlyGrowth, completed: boolean) => {
|
const formattedData = (data: GetUserWithDocumentMonthlyGrowth, completed: boolean) => {
|
||||||
return [...data].reverse().map(({ month, count, signed_count }) => {
|
return [...data].reverse().map(({ month, count, signed_count }) => {
|
||||||
const formattedMonth = DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL');
|
const formattedMonth = DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL');
|
||||||
28
apps/remix/app/components/general/app-banner.tsx
Normal file
28
apps/remix/app/components/general/app-banner.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { type TSiteSettingsBannerSchema } from '@documenso/lib/server-only/site-settings/schemas/banner';
|
||||||
|
|
||||||
|
export type AppBannerProps = {
|
||||||
|
banner: TSiteSettingsBannerSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppBanner = ({ banner }: AppBannerProps) => {
|
||||||
|
if (!banner.enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-2" style={{ background: banner.data.bgColor }}>
|
||||||
|
<div
|
||||||
|
className="mx-auto flex h-auto max-w-screen-xl items-center justify-center px-4 py-3 text-sm font-medium"
|
||||||
|
style={{ color: banner.data.textColor }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span dangerouslySetInnerHTML={{ __html: banner.data.content }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Banner
|
||||||
|
// Custom Text
|
||||||
|
// Custom Text with Custom Icon
|
||||||
@ -1,15 +1,13 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import type { MessageDescriptor } from '@lingui/core';
|
import type { MessageDescriptor } from '@lingui/core';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { CheckIcon, Loader, Monitor, Moon, Sun } from 'lucide-react';
|
import { CheckIcon, Loader, Monitor, Moon, Sun } from 'lucide-react';
|
||||||
import { useTheme } from 'next-themes';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
import { Theme, useTheme } from 'remix-themes';
|
||||||
|
|
||||||
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
||||||
import {
|
import {
|
||||||
@ -21,7 +19,6 @@ import {
|
|||||||
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
SKIP_QUERY_BATCH_META,
|
SKIP_QUERY_BATCH_META,
|
||||||
} from '@documenso/lib/constants/trpc';
|
} from '@documenso/lib/constants/trpc';
|
||||||
import { switchI18NLanguage } from '@documenso/lib/server-only/i18n/switch-i18n-language';
|
|
||||||
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@ -34,7 +31,6 @@ import {
|
|||||||
CommandList,
|
CommandList,
|
||||||
CommandShortcut,
|
CommandShortcut,
|
||||||
} from '@documenso/ui/primitives/command';
|
} from '@documenso/ui/primitives/command';
|
||||||
import { THEMES_TYPE } from '@documenso/ui/primitives/constants';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
const DOCUMENTS_PAGES = [
|
const DOCUMENTS_PAGES = [
|
||||||
@ -70,16 +66,15 @@ const SETTINGS_PAGES = [
|
|||||||
{ label: msg`Password`, path: '/settings/password' },
|
{ label: msg`Password`, path: '/settings/password' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export type CommandMenuProps = {
|
export type AppCommandMenuProps = {
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
onOpenChange?: (_open: boolean) => void;
|
onOpenChange?: (_open: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
export function AppCommandMenu({ open, onOpenChange }: AppCommandMenuProps) {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { setTheme } = useTheme();
|
|
||||||
|
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(() => open ?? false);
|
const [isOpen, setIsOpen] = useState(() => open ?? false);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
@ -138,10 +133,10 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
|||||||
|
|
||||||
const push = useCallback(
|
const push = useCallback(
|
||||||
(path: string) => {
|
(path: string) => {
|
||||||
router.push(path);
|
void navigate(path);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
},
|
},
|
||||||
[router, setOpen],
|
[setOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
const addPage = (page: string) => {
|
const addPage = (page: string) => {
|
||||||
@ -227,7 +222,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentPage === 'theme' && <ThemeCommands setTheme={setTheme} />}
|
{currentPage === 'theme' && <ThemeCommands />}
|
||||||
{currentPage === 'language' && <LanguageCommands />}
|
{currentPage === 'language' && <LanguageCommands />}
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandDialog>
|
</CommandDialog>
|
||||||
@ -256,19 +251,18 @@ const Commands = ({
|
|||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
const ThemeCommands = ({ setTheme }: { setTheme: (_theme: string) => void }) => {
|
const ThemeCommands = () => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const THEMES = useMemo(
|
const [, setTheme] = useTheme();
|
||||||
() => [
|
|
||||||
{ label: msg`Light Mode`, theme: THEMES_TYPE.LIGHT, icon: Sun },
|
|
||||||
{ label: msg`Dark Mode`, theme: THEMES_TYPE.DARK, icon: Moon },
|
|
||||||
{ label: msg`System Theme`, theme: THEMES_TYPE.SYSTEM, icon: Monitor },
|
|
||||||
],
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
return THEMES.map((theme) => (
|
const themes = [
|
||||||
|
{ label: msg`Light Mode`, theme: Theme.LIGHT, icon: Sun },
|
||||||
|
{ label: msg`Dark Mode`, theme: Theme.DARK, icon: Moon },
|
||||||
|
{ label: msg`System Theme`, theme: null, icon: Monitor },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
return themes.map((theme) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={theme.theme}
|
key={theme.theme}
|
||||||
onSelect={() => setTheme(theme.theme)}
|
onSelect={() => setTheme(theme.theme)}
|
||||||
@ -294,9 +288,23 @@ const LanguageCommands = () => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await dynamicActivate(i18n, lang);
|
await dynamicActivate(lang);
|
||||||
await switchI18NLanguage(lang);
|
|
||||||
} catch (err) {
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('lang', lang);
|
||||||
|
|
||||||
|
const response = await fetch('/api/locale', {
|
||||||
|
method: 'post',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to set language: ${e}`);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
@ -1,32 +1,28 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { type HTMLAttributes, useEffect, useState } from 'react';
|
import { type HTMLAttributes, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useParams } from 'next/navigation';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
|
|
||||||
import { MenuIcon, SearchIcon } from 'lucide-react';
|
import { MenuIcon, SearchIcon } from 'lucide-react';
|
||||||
|
import { Link, useLocation, useParams } from 'react-router';
|
||||||
|
|
||||||
|
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
||||||
import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
|
import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
|
||||||
import { getRootHref } from '@documenso/lib/utils/params';
|
import { getRootHref } from '@documenso/lib/utils/params';
|
||||||
import type { User } from '@documenso/prisma/client';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
import { Logo } from '~/components/branding/logo';
|
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||||
|
|
||||||
import { CommandMenu } from '../common/command-menu';
|
import { AppCommandMenu } from './app-command-menu';
|
||||||
import { DesktopNav } from './desktop-nav';
|
import { AppNavDesktop } from './app-nav-desktop';
|
||||||
|
import { AppNavMobile } from './app-nav-mobile';
|
||||||
import { MenuSwitcher } from './menu-switcher';
|
import { MenuSwitcher } from './menu-switcher';
|
||||||
import { MobileNavigation } from './mobile-navigation';
|
|
||||||
|
|
||||||
export type HeaderProps = HTMLAttributes<HTMLDivElement> & {
|
export type HeaderProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
user: User;
|
user: SessionUser;
|
||||||
teams: TGetTeamsResponse;
|
teams: TGetTeamsResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
const [isCommandMenuOpen, setIsCommandMenuOpen] = useState(false);
|
const [isCommandMenuOpen, setIsCommandMenuOpen] = useState(false);
|
||||||
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
|
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
|
||||||
@ -42,8 +38,6 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
|||||||
return () => window.removeEventListener('scroll', onScroll);
|
return () => window.removeEventListener('scroll', onScroll);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const isPathTeamUrl = (teamUrl: string) => {
|
const isPathTeamUrl = (teamUrl: string) => {
|
||||||
if (!pathname || !pathname.startsWith(`/t/`)) {
|
if (!pathname || !pathname.startsWith(`/t/`)) {
|
||||||
return false;
|
return false;
|
||||||
@ -65,13 +59,13 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
|||||||
>
|
>
|
||||||
<div className="mx-auto flex w-full max-w-screen-xl items-center justify-between gap-x-4 px-4 md:justify-normal md:px-8">
|
<div className="mx-auto flex w-full max-w-screen-xl items-center justify-between gap-x-4 px-4 md:justify-normal md:px-8">
|
||||||
<Link
|
<Link
|
||||||
href={`${getRootHref(params, { returnEmptyRootString: true })}/documents`}
|
to={`${getRootHref(params, { returnEmptyRootString: true })}/documents`}
|
||||||
className="focus-visible:ring-ring ring-offset-background hidden rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 md:inline"
|
className="focus-visible:ring-ring ring-offset-background hidden rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 md:inline"
|
||||||
>
|
>
|
||||||
<Logo className="h-6 w-auto" />
|
<BrandingLogo className="h-6 w-auto" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<DesktopNav setIsCommandMenuOpen={setIsCommandMenuOpen} />
|
<AppNavDesktop setIsCommandMenuOpen={setIsCommandMenuOpen} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex gap-x-4 md:ml-8"
|
className="flex gap-x-4 md:ml-8"
|
||||||
@ -89,9 +83,9 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
|||||||
<MenuIcon className="text-muted-foreground h-6 w-6" />
|
<MenuIcon className="text-muted-foreground h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<CommandMenu open={isCommandMenuOpen} onOpenChange={setIsCommandMenuOpen} />
|
<AppCommandMenu open={isCommandMenuOpen} onOpenChange={setIsCommandMenuOpen} />
|
||||||
|
|
||||||
<MobileNavigation
|
<AppNavMobile
|
||||||
isMenuOpen={isHamburgerMenuOpen}
|
isMenuOpen={isHamburgerMenuOpen}
|
||||||
onMenuOpenChange={setIsHamburgerMenuOpen}
|
onMenuOpenChange={setIsHamburgerMenuOpen}
|
||||||
/>
|
/>
|
||||||
@ -1,12 +1,11 @@
|
|||||||
import type { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useParams, usePathname } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
|
import { Link, useLocation, useParams } from 'react-router';
|
||||||
|
|
||||||
import { getRootHref } from '@documenso/lib/utils/params';
|
import { getRootHref } from '@documenso/lib/utils/params';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@ -23,14 +22,18 @@ const navigationLinks = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export type DesktopNavProps = HTMLAttributes<HTMLDivElement> & {
|
export type AppNavDesktopProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
setIsCommandMenuOpen: (value: boolean) => void;
|
setIsCommandMenuOpen: (value: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: DesktopNavProps) => {
|
export const AppNavDesktop = ({
|
||||||
|
className,
|
||||||
|
setIsCommandMenuOpen,
|
||||||
|
...props
|
||||||
|
}: AppNavDesktopProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const pathname = usePathname();
|
const { pathname } = useLocation();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const [modifierKey, setModifierKey] = useState(() => 'Ctrl');
|
const [modifierKey, setModifierKey] = useState(() => 'Ctrl');
|
||||||
@ -56,7 +59,7 @@ export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: Deskto
|
|||||||
{navigationLinks.map(({ href, label }) => (
|
{navigationLinks.map(({ href, label }) => (
|
||||||
<Link
|
<Link
|
||||||
key={href}
|
key={href}
|
||||||
href={`${rootHref}${href}`}
|
to={`${rootHref}${href}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground dark:text-muted-foreground/60 focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2',
|
'text-muted-foreground dark:text-muted-foreground/60 focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2',
|
||||||
{
|
{
|
||||||
@ -82,7 +85,7 @@ export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: Deskto
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-muted-foreground bg-muted flex items-center rounded-md px-1.5 py-0.5 text-xs tracking-wider">
|
<div className="text-muted-foreground bg-muted flex items-center rounded-md px-1.5 py-0.5 text-xs tracking-wider">
|
||||||
{modifierKey}+K
|
{modifierKey}+K
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1,24 +1,20 @@
|
|||||||
'use client';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useParams } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { signOut } from 'next-auth/react';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { Link, useParams } from 'react-router';
|
||||||
|
|
||||||
import LogoImage from '@documenso/assets/logo.png';
|
import LogoImage from '@documenso/assets/logo.png';
|
||||||
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { getRootHref } from '@documenso/lib/utils/params';
|
import { getRootHref } from '@documenso/lib/utils/params';
|
||||||
import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet';
|
import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet';
|
||||||
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
|
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
|
||||||
|
|
||||||
export type MobileNavigationProps = {
|
export type AppNavMobileProps = {
|
||||||
isMenuOpen: boolean;
|
isMenuOpen: boolean;
|
||||||
onMenuOpenChange?: (_value: boolean) => void;
|
onMenuOpenChange?: (_value: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigationProps) => {
|
export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@ -51,8 +47,8 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
|||||||
return (
|
return (
|
||||||
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
|
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
|
||||||
<SheetContent className="flex w-full max-w-[350px] flex-col">
|
<SheetContent className="flex w-full max-w-[350px] flex-col">
|
||||||
<Link href="/" onClick={handleMenuItemClick}>
|
<Link to="/" onClick={handleMenuItemClick}>
|
||||||
<Image
|
<img
|
||||||
src={LogoImage}
|
src={LogoImage}
|
||||||
alt="Documenso Logo"
|
alt="Documenso Logo"
|
||||||
className="dark:invert"
|
className="dark:invert"
|
||||||
@ -66,7 +62,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
|||||||
<Link
|
<Link
|
||||||
key={href}
|
key={href}
|
||||||
className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
|
className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
|
||||||
href={href}
|
to={href}
|
||||||
onClick={() => handleMenuItemClick()}
|
onClick={() => handleMenuItemClick()}
|
||||||
>
|
>
|
||||||
{_(text)}
|
{_(text)}
|
||||||
@ -75,11 +71,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
|
className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
|
||||||
onClick={async () =>
|
onClick={async () => authClient.signOut()}
|
||||||
signOut({
|
|
||||||
callbackUrl: '/',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Trans>Sign Out</Trans>
|
<Trans>Sign Out</Trans>
|
||||||
</button>
|
</button>
|
||||||
@ -1,17 +1,13 @@
|
|||||||
'use client';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import type { Recipient } from '@prisma/client';
|
||||||
|
import { DocumentStatus } from '@prisma/client';
|
||||||
|
|
||||||
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
||||||
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
@ -2,7 +2,7 @@ import type { SVGAttributes } from 'react';
|
|||||||
|
|
||||||
export type LogoProps = SVGAttributes<SVGSVGElement>;
|
export type LogoProps = SVGAttributes<SVGSVGElement>;
|
||||||
|
|
||||||
export const Logo = ({ ...props }: LogoProps) => {
|
export const BrandingLogo = ({ ...props }: LogoProps) => {
|
||||||
return (
|
return (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2248 320" {...props}>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2248 320" {...props}>
|
||||||
<path
|
<path
|
||||||
@ -1,16 +1,14 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -25,7 +23,7 @@ import { Input } from '@documenso/ui/primitives/input';
|
|||||||
import { PasswordInput } from '@documenso/ui/primitives/password-input';
|
import { PasswordInput } from '@documenso/ui/primitives/password-input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { signupErrorMessages } from '~/components/forms/v2/signup';
|
import { signupErrorMessages } from '~/components/forms/signup';
|
||||||
|
|
||||||
export type ClaimAccountProps = {
|
export type ClaimAccountProps = {
|
||||||
defaultName: string;
|
defaultName: string;
|
||||||
@ -60,9 +58,7 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const analytics = useAnalytics();
|
const analytics = useAnalytics();
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<TClaimAccountFormSchema>({
|
const form = useForm<TClaimAccountFormSchema>({
|
||||||
values: {
|
values: {
|
||||||
@ -75,9 +71,9 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
|
|||||||
|
|
||||||
const onFormSubmit = async ({ name, email, password }: TClaimAccountFormSchema) => {
|
const onFormSubmit = async ({ name, email, password }: TClaimAccountFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await signup({ name, email, password });
|
await authClient.emailPassword.signUp({ name, email, password });
|
||||||
|
|
||||||
router.push(`/unverified-account`);
|
await navigate(`/unverified-account`);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Registration Successful`),
|
title: _(msg`Registration Successful`),
|
||||||
@ -1,15 +1,14 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Recipient } from '@prisma/client';
|
||||||
|
import type { Field } from '@prisma/client';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import type { TTemplate } from '@documenso/lib/types/template';
|
import type { TTemplate } from '@documenso/lib/types/template';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
|
||||||
import type { Field } from '@documenso/prisma/client';
|
|
||||||
import {
|
import {
|
||||||
DocumentFlowFormContainerActions,
|
DocumentFlowFormContainerActions,
|
||||||
DocumentFlowFormContainerContent,
|
DocumentFlowFormContainerContent,
|
||||||
@ -30,36 +29,37 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useStep } from '@documenso/ui/primitives/stepper';
|
import { useStep } from '@documenso/ui/primitives/stepper';
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from '~/app/(signing)/sign/[token]/document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from '~/components/general/document-signing/document-signing-auth-provider';
|
||||||
|
|
||||||
const ZConfigureDirectTemplateFormSchema = z.object({
|
const ZDirectTemplateConfigureFormSchema = z.object({
|
||||||
email: z.string().email('Email is invalid'),
|
email: z.string().email('Email is invalid'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TConfigureDirectTemplateFormSchema = z.infer<typeof ZConfigureDirectTemplateFormSchema>;
|
export type TDirectTemplateConfigureFormSchema = z.infer<typeof ZDirectTemplateConfigureFormSchema>;
|
||||||
|
|
||||||
export type ConfigureDirectTemplateFormProps = {
|
export type DirectTemplateConfigureFormProps = {
|
||||||
flowStep: DocumentFlowStep;
|
flowStep: DocumentFlowStep;
|
||||||
isDocumentPdfLoaded: boolean;
|
isDocumentPdfLoaded: boolean;
|
||||||
template: Omit<TTemplate, 'user'>;
|
template: Omit<TTemplate, 'user'>;
|
||||||
directTemplateRecipient: Recipient & { fields: Field[] };
|
directTemplateRecipient: Recipient & { fields: Field[] };
|
||||||
initialEmail?: string;
|
initialEmail?: string;
|
||||||
onSubmit: (_data: TConfigureDirectTemplateFormSchema) => void;
|
onSubmit: (_data: TDirectTemplateConfigureFormSchema) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ConfigureDirectTemplateFormPartial = ({
|
export const DirectTemplateConfigureForm = ({
|
||||||
flowStep,
|
flowStep,
|
||||||
isDocumentPdfLoaded,
|
isDocumentPdfLoaded,
|
||||||
template,
|
template,
|
||||||
directTemplateRecipient,
|
directTemplateRecipient,
|
||||||
initialEmail,
|
initialEmail,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: ConfigureDirectTemplateFormProps) => {
|
}: DirectTemplateConfigureFormProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { data: session } = useSession();
|
|
||||||
|
const { user } = useOptionalSession();
|
||||||
|
|
||||||
const { recipients } = template;
|
const { recipients } = template;
|
||||||
const { derivedRecipientAccessAuth } = useRequiredDocumentAuthContext();
|
const { derivedRecipientAccessAuth } = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
const recipientsWithBlankDirectRecipientEmail = recipients.map((recipient) => {
|
const recipientsWithBlankDirectRecipientEmail = recipients.map((recipient) => {
|
||||||
if (recipient.id === directTemplateRecipient.id) {
|
if (recipient.id === directTemplateRecipient.id) {
|
||||||
@ -72,9 +72,9 @@ export const ConfigureDirectTemplateFormPartial = ({
|
|||||||
return recipient;
|
return recipient;
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<TConfigureDirectTemplateFormSchema>({
|
const form = useForm<TDirectTemplateConfigureFormSchema>({
|
||||||
resolver: zodResolver(
|
resolver: zodResolver(
|
||||||
ZConfigureDirectTemplateFormSchema.superRefine((items, ctx) => {
|
ZDirectTemplateConfigureFormSchema.superRefine((items, ctx) => {
|
||||||
if (template.recipients.map((recipient) => recipient.email).includes(items.email)) {
|
if (template.recipients.map((recipient) => recipient.email).includes(items.email)) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
@ -125,7 +125,7 @@ export const ConfigureDirectTemplateFormPartial = ({
|
|||||||
disabled={
|
disabled={
|
||||||
field.disabled ||
|
field.disabled ||
|
||||||
derivedRecipientAccessAuth !== null ||
|
derivedRecipientAccessAuth !== null ||
|
||||||
session?.user.email !== undefined
|
user?.email !== undefined
|
||||||
}
|
}
|
||||||
placeholder="recipient@documenso.com"
|
placeholder="recipient@documenso.com"
|
||||||
/>
|
/>
|
||||||
@ -1,16 +1,13 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import type { Field } from '@prisma/client';
|
||||||
|
import { type Recipient } from '@prisma/client';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router';
|
||||||
|
|
||||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||||
import type { TTemplate } from '@documenso/lib/types/template';
|
import type { TTemplate } from '@documenso/lib/types/template';
|
||||||
import type { Field } from '@documenso/prisma/client';
|
|
||||||
import { type Recipient } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
|
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
|
||||||
@ -19,15 +16,19 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
|||||||
import { Stepper } from '@documenso/ui/primitives/stepper';
|
import { Stepper } from '@documenso/ui/primitives/stepper';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from '~/app/(signing)/sign/[token]/document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from '~/components/general/document-signing/document-signing-auth-provider';
|
||||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
import { useRequiredDocumentSigningContext } from '~/components/general/document-signing/document-signing-provider';
|
||||||
|
|
||||||
import type { TConfigureDirectTemplateFormSchema } from './configure-direct-template';
|
import {
|
||||||
import { ConfigureDirectTemplateFormPartial } from './configure-direct-template';
|
DirectTemplateConfigureForm,
|
||||||
import type { DirectTemplateLocalField } from './sign-direct-template';
|
type TDirectTemplateConfigureFormSchema,
|
||||||
import { SignDirectTemplateForm } from './sign-direct-template';
|
} from './direct-template-configure-form';
|
||||||
|
import {
|
||||||
|
type DirectTemplateLocalField,
|
||||||
|
DirectTemplateSigningForm,
|
||||||
|
} from './direct-template-signing-form';
|
||||||
|
|
||||||
export type TemplatesDirectPageViewProps = {
|
export type DirectTemplatePageViewProps = {
|
||||||
template: Omit<TTemplate, 'user'>;
|
template: Omit<TTemplate, 'user'>;
|
||||||
directTemplateToken: string;
|
directTemplateToken: string;
|
||||||
directTemplateRecipient: Recipient & { fields: Field[] };
|
directTemplateRecipient: Recipient & { fields: Field[] };
|
||||||
@ -40,15 +41,15 @@ export const DirectTemplatePageView = ({
|
|||||||
template,
|
template,
|
||||||
directTemplateRecipient,
|
directTemplateRecipient,
|
||||||
directTemplateToken,
|
directTemplateToken,
|
||||||
}: TemplatesDirectPageViewProps) => {
|
}: DirectTemplatePageViewProps) => {
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const searchParams = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { email, fullName, setEmail } = useRequiredSigningContext();
|
const { email, fullName, setEmail } = useRequiredDocumentSigningContext();
|
||||||
const { recipient, setRecipient } = useRequiredDocumentAuthContext();
|
const { recipient, setRecipient } = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
const [step, setStep] = useState<DirectTemplateStep>('configure');
|
const [step, setStep] = useState<DirectTemplateStep>('configure');
|
||||||
const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false);
|
const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false);
|
||||||
@ -76,7 +77,7 @@ export const DirectTemplatePageView = ({
|
|||||||
/**
|
/**
|
||||||
* Set the email into a temporary recipient so it can be used for reauth and signing email fields.
|
* Set the email into a temporary recipient so it can be used for reauth and signing email fields.
|
||||||
*/
|
*/
|
||||||
const onConfigureDirectTemplateSubmit = ({ email }: TConfigureDirectTemplateFormSchema) => {
|
const onConfigureDirectTemplateSubmit = ({ email }: TDirectTemplateConfigureFormSchema) => {
|
||||||
setEmail(email);
|
setEmail(email);
|
||||||
|
|
||||||
setRecipient({
|
setRecipient({
|
||||||
@ -112,7 +113,7 @@ export const DirectTemplatePageView = ({
|
|||||||
|
|
||||||
const redirectUrl = template.templateMeta?.redirectUrl;
|
const redirectUrl = template.templateMeta?.redirectUrl;
|
||||||
|
|
||||||
redirectUrl ? router.push(redirectUrl) : router.push(`/sign/${token}/complete`);
|
await (redirectUrl ? navigate(redirectUrl) : navigate(`/sign/${token}/complete`));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Something went wrong`),
|
title: _(msg`Something went wrong`),
|
||||||
@ -152,7 +153,7 @@ export const DirectTemplatePageView = ({
|
|||||||
currentStep={currentDocumentFlow.stepIndex}
|
currentStep={currentDocumentFlow.stepIndex}
|
||||||
setCurrentStep={(step) => setStep(DirectTemplateSteps[step - 1])}
|
setCurrentStep={(step) => setStep(DirectTemplateSteps[step - 1])}
|
||||||
>
|
>
|
||||||
<ConfigureDirectTemplateFormPartial
|
<DirectTemplateConfigureForm
|
||||||
flowStep={directTemplateFlow.configure}
|
flowStep={directTemplateFlow.configure}
|
||||||
template={template}
|
template={template}
|
||||||
directTemplateRecipient={directTemplateRecipient}
|
directTemplateRecipient={directTemplateRecipient}
|
||||||
@ -161,7 +162,7 @@ export const DirectTemplatePageView = ({
|
|||||||
initialEmail={email}
|
initialEmail={email}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SignDirectTemplateForm
|
<DirectTemplateSigningForm
|
||||||
flowStep={directTemplateFlow.sign}
|
flowStep={directTemplateFlow.sign}
|
||||||
directRecipient={recipient}
|
directRecipient={recipient}
|
||||||
directRecipientFields={directTemplateRecipient.fields}
|
directRecipientFields={directTemplateRecipient.fields}
|
||||||
@ -1,11 +1,10 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { signOut } from 'next-auth/react';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
|
||||||
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
@ -19,9 +18,7 @@ export const DirectTemplateAuthPageView = () => {
|
|||||||
try {
|
try {
|
||||||
setIsSigningOut(true);
|
setIsSigningOut(true);
|
||||||
|
|
||||||
await signOut({
|
await authClient.signOut();
|
||||||
callbackUrl: '/signin',
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Something went wrong`),
|
title: _(msg`Something went wrong`),
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Field, Recipient, Signature } from '@prisma/client';
|
||||||
|
import { FieldType } from '@prisma/client';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
@ -16,8 +18,6 @@ import {
|
|||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
import type { TTemplate } from '@documenso/lib/types/template';
|
import type { TTemplate } from '@documenso/lib/types/template';
|
||||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import type { Field, Recipient, Signature } from '@documenso/prisma/client';
|
|
||||||
import { FieldType } from '@documenso/prisma/client';
|
|
||||||
import type {
|
import type {
|
||||||
TRemovedSignedFieldWithTokenMutationSchema,
|
TRemovedSignedFieldWithTokenMutationSchema,
|
||||||
TSignFieldWithTokenMutationSchema,
|
TSignFieldWithTokenMutationSchema,
|
||||||
@ -38,20 +38,20 @@ import { Label } from '@documenso/ui/primitives/label';
|
|||||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useStep } from '@documenso/ui/primitives/stepper';
|
import { useStep } from '@documenso/ui/primitives/stepper';
|
||||||
|
|
||||||
import { CheckboxField } from '~/app/(signing)/sign/[token]/checkbox-field';
|
import { DocumentSigningCheckboxField } from '~/components/general/document-signing/document-signing-checkbox-field';
|
||||||
import { DateField } from '~/app/(signing)/sign/[token]/date-field';
|
import { DocumentSigningCompleteDialog } from '~/components/general/document-signing/document-signing-complete-dialog';
|
||||||
import { DropdownField } from '~/app/(signing)/sign/[token]/dropdown-field';
|
import { DocumentSigningDateField } from '~/components/general/document-signing/document-signing-date-field';
|
||||||
import { EmailField } from '~/app/(signing)/sign/[token]/email-field';
|
import { DocumentSigningDropdownField } from '~/components/general/document-signing/document-signing-dropdown-field';
|
||||||
import { InitialsField } from '~/app/(signing)/sign/[token]/initials-field';
|
import { DocumentSigningEmailField } from '~/components/general/document-signing/document-signing-email-field';
|
||||||
import { NameField } from '~/app/(signing)/sign/[token]/name-field';
|
import { DocumentSigningInitialsField } from '~/components/general/document-signing/document-signing-initials-field';
|
||||||
import { NumberField } from '~/app/(signing)/sign/[token]/number-field';
|
import { DocumentSigningNameField } from '~/components/general/document-signing/document-signing-name-field';
|
||||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
import { DocumentSigningNumberField } from '~/components/general/document-signing/document-signing-number-field';
|
||||||
import { RadioField } from '~/app/(signing)/sign/[token]/radio-field';
|
import { useRequiredDocumentSigningContext } from '~/components/general/document-signing/document-signing-provider';
|
||||||
import { SignDialog } from '~/app/(signing)/sign/[token]/sign-dialog';
|
import { DocumentSigningRadioField } from '~/components/general/document-signing/document-signing-radio-field';
|
||||||
import { SignatureField } from '~/app/(signing)/sign/[token]/signature-field';
|
import { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-field';
|
||||||
import { TextField } from '~/app/(signing)/sign/[token]/text-field';
|
import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
|
||||||
|
|
||||||
export type SignDirectTemplateFormProps = {
|
export type DirectTemplateSigningFormProps = {
|
||||||
flowStep: DocumentFlowStep;
|
flowStep: DocumentFlowStep;
|
||||||
directRecipient: Recipient;
|
directRecipient: Recipient;
|
||||||
directRecipientFields: Field[];
|
directRecipientFields: Field[];
|
||||||
@ -64,15 +64,15 @@ export type DirectTemplateLocalField = Field & {
|
|||||||
signature?: Signature;
|
signature?: Signature;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SignDirectTemplateForm = ({
|
export const DirectTemplateSigningForm = ({
|
||||||
flowStep,
|
flowStep,
|
||||||
directRecipient,
|
directRecipient,
|
||||||
directRecipientFields,
|
directRecipientFields,
|
||||||
template,
|
template,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: SignDirectTemplateFormProps) => {
|
}: DirectTemplateSigningFormProps) => {
|
||||||
const { fullName, signature, signatureValid, setFullName, setSignature } =
|
const { fullName, signature, signatureValid, setFullName, setSignature } =
|
||||||
useRequiredSigningContext();
|
useRequiredDocumentSigningContext();
|
||||||
|
|
||||||
const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(directRecipientFields);
|
const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(directRecipientFields);
|
||||||
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||||
@ -183,7 +183,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
{localFields.map((field) =>
|
{localFields.map((field) =>
|
||||||
match(field.type)
|
match(field.type)
|
||||||
.with(FieldType.SIGNATURE, () => (
|
.with(FieldType.SIGNATURE, () => (
|
||||||
<SignatureField
|
<DocumentSigningSignatureField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={directRecipient}
|
recipient={directRecipient}
|
||||||
@ -192,7 +192,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.INITIALS, () => (
|
.with(FieldType.INITIALS, () => (
|
||||||
<InitialsField
|
<DocumentSigningInitialsField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={directRecipient}
|
recipient={directRecipient}
|
||||||
@ -201,7 +201,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.NAME, () => (
|
.with(FieldType.NAME, () => (
|
||||||
<NameField
|
<DocumentSigningNameField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={directRecipient}
|
recipient={directRecipient}
|
||||||
@ -210,7 +210,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.DATE, () => (
|
.with(FieldType.DATE, () => (
|
||||||
<DateField
|
<DocumentSigningDateField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={directRecipient}
|
recipient={directRecipient}
|
||||||
@ -221,7 +221,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.EMAIL, () => (
|
.with(FieldType.EMAIL, () => (
|
||||||
<EmailField
|
<DocumentSigningEmailField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={directRecipient}
|
recipient={directRecipient}
|
||||||
@ -235,7 +235,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<DocumentSigningTextField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={{
|
field={{
|
||||||
...field,
|
...field,
|
||||||
@ -253,7 +253,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NumberField
|
<DocumentSigningNumberField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={{
|
field={{
|
||||||
...field,
|
...field,
|
||||||
@ -271,7 +271,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownField
|
<DocumentSigningDropdownField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={{
|
field={{
|
||||||
...field,
|
...field,
|
||||||
@ -289,7 +289,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RadioField
|
<DocumentSigningRadioField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={{
|
field={{
|
||||||
...field,
|
...field,
|
||||||
@ -307,7 +307,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CheckboxField
|
<DocumentSigningCheckboxField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={{
|
field={{
|
||||||
...field,
|
...field,
|
||||||
@ -373,7 +373,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
<Trans>Back</Trans>
|
<Trans>Back</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<SignDialog
|
<DocumentSigningCompleteDialog
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
onSignatureComplete={handleSubmit}
|
onSignatureComplete={handleSubmit}
|
||||||
documentTitle={template.title}
|
documentTitle={template.title}
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { RecipientRole } from '@prisma/client';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { DocumentAuth, type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import { DocumentAuth, type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { RecipientRole } from '@documenso/prisma/client';
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { DialogFooter } from '@documenso/ui/primitives/dialog';
|
import { DialogFooter } from '@documenso/ui/primitives/dialog';
|
||||||
@ -23,9 +23,9 @@ import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/
|
|||||||
|
|
||||||
import { EnableAuthenticatorAppDialog } from '~/components/forms/2fa/enable-authenticator-app-dialog';
|
import { EnableAuthenticatorAppDialog } from '~/components/forms/2fa/enable-authenticator-app-dialog';
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
|
|
||||||
export type DocumentActionAuth2FAProps = {
|
export type DocumentSigningAuth2FAProps = {
|
||||||
actionTarget?: 'FIELD' | 'DOCUMENT';
|
actionTarget?: 'FIELD' | 'DOCUMENT';
|
||||||
actionVerb?: string;
|
actionVerb?: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -42,15 +42,15 @@ const Z2FAAuthFormSchema = z.object({
|
|||||||
|
|
||||||
type T2FAAuthFormSchema = z.infer<typeof Z2FAAuthFormSchema>;
|
type T2FAAuthFormSchema = z.infer<typeof Z2FAAuthFormSchema>;
|
||||||
|
|
||||||
export const DocumentActionAuth2FA = ({
|
export const DocumentSigningAuth2FA = ({
|
||||||
actionTarget = 'FIELD',
|
actionTarget = 'FIELD',
|
||||||
actionVerb = 'sign',
|
actionVerb = 'sign',
|
||||||
onReauthFormSubmit,
|
onReauthFormSubmit,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: DocumentActionAuth2FAProps) => {
|
}: DocumentSigningAuth2FAProps) => {
|
||||||
const { recipient, user, isCurrentlyAuthenticating, setIsCurrentlyAuthenticating } =
|
const { recipient, user, isCurrentlyAuthenticating, setIsCurrentlyAuthenticating } =
|
||||||
useRequiredDocumentAuthContext();
|
useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
const form = useForm<T2FAAuthFormSchema>({
|
const form = useForm<T2FAAuthFormSchema>({
|
||||||
resolver: zodResolver(Z2FAAuthFormSchema),
|
resolver: zodResolver(Z2FAAuthFormSchema),
|
||||||
@ -109,17 +109,14 @@ export const DocumentActionAuth2FA = ({
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{user?.identityProvider === 'DOCUMENSO' && (
|
<p className="mt-2">
|
||||||
<p className="mt-2">
|
<Trans>
|
||||||
<Trans>
|
By enabling 2FA, you will be required to enter a code from your authenticator app
|
||||||
By enabling 2FA, you will be required to enter a code from your authenticator app
|
every time you sign in using email password.
|
||||||
every time you sign in.
|
</Trans>
|
||||||
</Trans>
|
</p>
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
||||||
<Trans>Close</Trans>
|
<Trans>Close</Trans>
|
||||||
@ -1,31 +1,32 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
|
import { RecipientRole } from '@prisma/client';
|
||||||
|
|
||||||
import { Trans } from '@lingui/macro';
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { signOut } from 'next-auth/react';
|
|
||||||
|
|
||||||
import { RecipientRole } from '@documenso/prisma/client';
|
|
||||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { DialogFooter } from '@documenso/ui/primitives/dialog';
|
import { DialogFooter } from '@documenso/ui/primitives/dialog';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
|
|
||||||
export type DocumentActionAuthAccountProps = {
|
export type DocumentSigningAuthAccountProps = {
|
||||||
actionTarget?: 'FIELD' | 'DOCUMENT';
|
actionTarget?: 'FIELD' | 'DOCUMENT';
|
||||||
actionVerb?: string;
|
actionVerb?: string;
|
||||||
onOpenChange: (value: boolean) => void;
|
onOpenChange: (value: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentActionAuthAccount = ({
|
export const DocumentSigningAuthAccount = ({
|
||||||
actionTarget = 'FIELD',
|
actionTarget = 'FIELD',
|
||||||
actionVerb = 'sign',
|
actionVerb = 'sign',
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: DocumentActionAuthAccountProps) => {
|
}: DocumentSigningAuthAccountProps) => {
|
||||||
const { recipient } = useRequiredDocumentAuthContext();
|
const { recipient } = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
const router = useRouter();
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
const [isSigningOut, setIsSigningOut] = useState(false);
|
const [isSigningOut, setIsSigningOut] = useState(false);
|
||||||
|
|
||||||
@ -33,15 +34,18 @@ export const DocumentActionAuthAccount = ({
|
|||||||
try {
|
try {
|
||||||
setIsSigningOut(true);
|
setIsSigningOut(true);
|
||||||
|
|
||||||
await signOut({
|
await authClient.signOut({
|
||||||
redirect: false,
|
redirectPath: `/signin#email=${email}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push(`/signin#email=${email}`);
|
|
||||||
} catch {
|
} catch {
|
||||||
setIsSigningOut(false);
|
setIsSigningOut(false);
|
||||||
|
|
||||||
// Todo: Alert.
|
toast({
|
||||||
|
title: t`Something went wrong`,
|
||||||
|
description: t`We were unable to log you out at this time.`,
|
||||||
|
duration: 10000,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { FieldType } from '@prisma/client';
|
||||||
import { P, match } from 'ts-pattern';
|
import { P, match } from 'ts-pattern';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -6,7 +7,6 @@ import {
|
|||||||
type TRecipientActionAuth,
|
type TRecipientActionAuth,
|
||||||
type TRecipientActionAuthTypes,
|
type TRecipientActionAuthTypes,
|
||||||
} from '@documenso/lib/types/document-auth';
|
} from '@documenso/lib/types/document-auth';
|
||||||
import type { FieldType } from '@documenso/prisma/client';
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -15,12 +15,12 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
|
||||||
import { DocumentActionAuth2FA } from './document-action-auth-2fa';
|
import { DocumentSigningAuth2FA } from './document-signing-auth-2fa';
|
||||||
import { DocumentActionAuthAccount } from './document-action-auth-account';
|
import { DocumentSigningAuthAccount } from './document-signing-auth-account';
|
||||||
import { DocumentActionAuthPasskey } from './document-action-auth-passkey';
|
import { DocumentSigningAuthPasskey } from './document-signing-auth-passkey';
|
||||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
|
|
||||||
export type DocumentActionAuthDialogProps = {
|
export type DocumentSigningAuthDialogProps = {
|
||||||
title?: string;
|
title?: string;
|
||||||
documentAuthType: TRecipientActionAuthTypes;
|
documentAuthType: TRecipientActionAuthTypes;
|
||||||
description?: string;
|
description?: string;
|
||||||
@ -34,15 +34,15 @@ export type DocumentActionAuthDialogProps = {
|
|||||||
onReauthFormSubmit: (values?: TRecipientActionAuth) => Promise<void> | void;
|
onReauthFormSubmit: (values?: TRecipientActionAuth) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentActionAuthDialog = ({
|
export const DocumentSigningAuthDialog = ({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
documentAuthType,
|
documentAuthType,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onReauthFormSubmit,
|
onReauthFormSubmit,
|
||||||
}: DocumentActionAuthDialogProps) => {
|
}: DocumentSigningAuthDialogProps) => {
|
||||||
const { recipient, user, isCurrentlyAuthenticating } = useRequiredDocumentAuthContext();
|
const { recipient, user, isCurrentlyAuthenticating } = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
const handleOnOpenChange = (value: boolean) => {
|
const handleOnOpenChange = (value: boolean) => {
|
||||||
if (isCurrentlyAuthenticating) {
|
if (isCurrentlyAuthenticating) {
|
||||||
@ -67,17 +67,17 @@ export const DocumentActionAuthDialog = ({
|
|||||||
.with(
|
.with(
|
||||||
{ documentAuthType: DocumentAuth.ACCOUNT },
|
{ documentAuthType: DocumentAuth.ACCOUNT },
|
||||||
{ user: P.when((user) => !user || user.email !== recipient.email) }, // Assume all current auth methods requires them to be logged in.
|
{ user: P.when((user) => !user || user.email !== recipient.email) }, // Assume all current auth methods requires them to be logged in.
|
||||||
() => <DocumentActionAuthAccount onOpenChange={onOpenChange} />,
|
() => <DocumentSigningAuthAccount onOpenChange={onOpenChange} />,
|
||||||
)
|
)
|
||||||
.with({ documentAuthType: DocumentAuth.PASSKEY }, () => (
|
.with({ documentAuthType: DocumentAuth.PASSKEY }, () => (
|
||||||
<DocumentActionAuthPasskey
|
<DocumentSigningAuthPasskey
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
onReauthFormSubmit={onReauthFormSubmit}
|
onReauthFormSubmit={onReauthFormSubmit}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with({ documentAuthType: DocumentAuth.TWO_FACTOR_AUTH }, () => (
|
.with({ documentAuthType: DocumentAuth.TWO_FACTOR_AUTH }, () => (
|
||||||
<DocumentActionAuth2FA
|
<DocumentSigningAuth2FA
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
onReauthFormSubmit={onReauthFormSubmit}
|
onReauthFormSubmit={onReauthFormSubmit}
|
||||||
@ -1,38 +1,34 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { signOut } from 'next-auth/react';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
|
||||||
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type SigningAuthPageViewProps = {
|
export type DocumentSigningAuthPageViewProps = {
|
||||||
email: string;
|
email: string;
|
||||||
emailHasAccount?: boolean;
|
emailHasAccount?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SigningAuthPageView = ({ email, emailHasAccount }: SigningAuthPageViewProps) => {
|
export const DocumentSigningAuthPageView = ({
|
||||||
|
email,
|
||||||
|
emailHasAccount,
|
||||||
|
}: DocumentSigningAuthPageViewProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [isSigningOut, setIsSigningOut] = useState(false);
|
const [isSigningOut, setIsSigningOut] = useState(false);
|
||||||
|
|
||||||
const handleChangeAccount = async (email: string) => {
|
const handleChangeAccount = async (email: string) => {
|
||||||
try {
|
try {
|
||||||
setIsSigningOut(true);
|
setIsSigningOut(true);
|
||||||
|
|
||||||
await signOut({
|
await authClient.signOut({
|
||||||
redirect: false,
|
redirectPath: emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push(emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`);
|
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Something went wrong`),
|
title: _(msg`Something went wrong`),
|
||||||
@ -1,8 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { RecipientRole } from '@prisma/client';
|
||||||
import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser';
|
import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
@ -10,7 +12,6 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { DocumentAuth, type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import { DocumentAuth, type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { RecipientRole } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -31,11 +32,11 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@documenso/ui/primitives/select';
|
} from '@documenso/ui/primitives/select';
|
||||||
|
|
||||||
import { CreatePasskeyDialog } from '~/app/(dashboard)/settings/security/passkeys/create-passkey-dialog';
|
import { PasskeyCreateDialog } from '~/components/dialogs/passkey-create-dialog';
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
|
|
||||||
export type DocumentActionAuthPasskeyProps = {
|
export type DocumentSigningAuthPasskeyProps = {
|
||||||
actionTarget?: 'FIELD' | 'DOCUMENT';
|
actionTarget?: 'FIELD' | 'DOCUMENT';
|
||||||
actionVerb?: string;
|
actionVerb?: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -49,13 +50,13 @@ const ZPasskeyAuthFormSchema = z.object({
|
|||||||
|
|
||||||
type TPasskeyAuthFormSchema = z.infer<typeof ZPasskeyAuthFormSchema>;
|
type TPasskeyAuthFormSchema = z.infer<typeof ZPasskeyAuthFormSchema>;
|
||||||
|
|
||||||
export const DocumentActionAuthPasskey = ({
|
export const DocumentSigningAuthPasskey = ({
|
||||||
actionTarget = 'FIELD',
|
actionTarget = 'FIELD',
|
||||||
actionVerb = 'sign',
|
actionVerb = 'sign',
|
||||||
onReauthFormSubmit,
|
onReauthFormSubmit,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: DocumentActionAuthPasskeyProps) => {
|
}: DocumentSigningAuthPasskeyProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -66,7 +67,7 @@ export const DocumentActionAuthPasskey = ({
|
|||||||
isCurrentlyAuthenticating,
|
isCurrentlyAuthenticating,
|
||||||
setIsCurrentlyAuthenticating,
|
setIsCurrentlyAuthenticating,
|
||||||
refetchPasskeys,
|
refetchPasskeys,
|
||||||
} = useRequiredDocumentAuthContext();
|
} = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
const form = useForm<TPasskeyAuthFormSchema>({
|
const form = useForm<TPasskeyAuthFormSchema>({
|
||||||
resolver: zodResolver(ZPasskeyAuthFormSchema),
|
resolver: zodResolver(ZPasskeyAuthFormSchema),
|
||||||
@ -189,7 +190,7 @@ export const DocumentActionAuthPasskey = ({
|
|||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<CreatePasskeyDialog
|
<PasskeyCreateDialog
|
||||||
onSuccess={async () => refetchPasskeys()}
|
onSuccess={async () => refetchPasskeys()}
|
||||||
trigger={
|
trigger={
|
||||||
<Button>
|
<Button>
|
||||||
@ -1,9 +1,9 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { type Document, FieldType, type Passkey, type Recipient } from '@prisma/client';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
|
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
||||||
import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth';
|
import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth';
|
||||||
import type {
|
import type {
|
||||||
TDocumentAuthOptions,
|
TDocumentAuthOptions,
|
||||||
@ -13,17 +13,10 @@ import type {
|
|||||||
} from '@documenso/lib/types/document-auth';
|
} from '@documenso/lib/types/document-auth';
|
||||||
import { DocumentAuth } from '@documenso/lib/types/document-auth';
|
import { DocumentAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||||
import {
|
|
||||||
type Document,
|
|
||||||
FieldType,
|
|
||||||
type Passkey,
|
|
||||||
type Recipient,
|
|
||||||
type User,
|
|
||||||
} from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
|
||||||
import type { DocumentActionAuthDialogProps } from './document-action-auth-dialog';
|
import type { DocumentSigningAuthDialogProps } from './document-signing-auth-dialog';
|
||||||
import { DocumentActionAuthDialog } from './document-action-auth-dialog';
|
import { DocumentSigningAuthDialog } from './document-signing-auth-dialog';
|
||||||
|
|
||||||
type PasskeyData = {
|
type PasskeyData = {
|
||||||
passkeys: Omit<Passkey, 'credentialId' | 'credentialPublicKey'>[];
|
passkeys: Omit<Passkey, 'credentialId' | 'credentialPublicKey'>[];
|
||||||
@ -32,7 +25,7 @@ type PasskeyData = {
|
|||||||
isError: boolean;
|
isError: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DocumentAuthContextValue = {
|
export type DocumentSigningAuthContextValue = {
|
||||||
executeActionAuthProcedure: (_value: ExecuteActionAuthProcedureOptions) => Promise<void>;
|
executeActionAuthProcedure: (_value: ExecuteActionAuthProcedureOptions) => Promise<void>;
|
||||||
documentAuthOptions: Document['authOptions'];
|
documentAuthOptions: Document['authOptions'];
|
||||||
documentAuthOption: TDocumentAuthOptions;
|
documentAuthOption: TDocumentAuthOptions;
|
||||||
@ -48,39 +41,39 @@ export type DocumentAuthContextValue = {
|
|||||||
passkeyData: PasskeyData;
|
passkeyData: PasskeyData;
|
||||||
preferredPasskeyId: string | null;
|
preferredPasskeyId: string | null;
|
||||||
setPreferredPasskeyId: (_value: string | null) => void;
|
setPreferredPasskeyId: (_value: string | null) => void;
|
||||||
user?: User | null;
|
user?: SessionUser | null;
|
||||||
refetchPasskeys: () => Promise<void>;
|
refetchPasskeys: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DocumentAuthContext = createContext<DocumentAuthContextValue | null>(null);
|
const DocumentSigningAuthContext = createContext<DocumentSigningAuthContextValue | null>(null);
|
||||||
|
|
||||||
export const useDocumentAuthContext = () => {
|
export const useDocumentSigningAuthContext = () => {
|
||||||
return useContext(DocumentAuthContext);
|
return useContext(DocumentSigningAuthContext);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useRequiredDocumentAuthContext = () => {
|
export const useRequiredDocumentSigningAuthContext = () => {
|
||||||
const context = useDocumentAuthContext();
|
const context = useDocumentSigningAuthContext();
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('Document auth context is required');
|
throw new Error('Document signing auth context is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface DocumentAuthProviderProps {
|
export interface DocumentSigningAuthProviderProps {
|
||||||
documentAuthOptions: Document['authOptions'];
|
documentAuthOptions: Document['authOptions'];
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
user?: User | null;
|
user?: SessionUser | null;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DocumentAuthProvider = ({
|
export const DocumentSigningAuthProvider = ({
|
||||||
documentAuthOptions: initialDocumentAuthOptions,
|
documentAuthOptions: initialDocumentAuthOptions,
|
||||||
recipient: initialRecipient,
|
recipient: initialRecipient,
|
||||||
user,
|
user,
|
||||||
children,
|
children,
|
||||||
}: DocumentAuthProviderProps) => {
|
}: DocumentSigningAuthProviderProps) => {
|
||||||
const [documentAuthOptions, setDocumentAuthOptions] = useState(initialDocumentAuthOptions);
|
const [documentAuthOptions, setDocumentAuthOptions] = useState(initialDocumentAuthOptions);
|
||||||
const [recipient, setRecipient] = useState(initialRecipient);
|
const [recipient, setRecipient] = useState(initialRecipient);
|
||||||
|
|
||||||
@ -186,7 +179,7 @@ export const DocumentAuthProvider = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentAuthContext.Provider
|
<DocumentSigningAuthContext.Provider
|
||||||
value={{
|
value={{
|
||||||
user,
|
user,
|
||||||
documentAuthOptions,
|
documentAuthOptions,
|
||||||
@ -210,7 +203,7 @@ export const DocumentAuthProvider = ({
|
|||||||
{children}
|
{children}
|
||||||
|
|
||||||
{documentAuthDialogPayload && derivedRecipientActionAuth && (
|
{documentAuthDialogPayload && derivedRecipientActionAuth && (
|
||||||
<DocumentActionAuthDialog
|
<DocumentSigningAuthDialog
|
||||||
open={true}
|
open={true}
|
||||||
onOpenChange={() => setDocumentAuthDialogPayload(null)}
|
onOpenChange={() => setDocumentAuthDialogPayload(null)}
|
||||||
onReauthFormSubmit={documentAuthDialogPayload.onReauthFormSubmit}
|
onReauthFormSubmit={documentAuthDialogPayload.onReauthFormSubmit}
|
||||||
@ -218,13 +211,13 @@ export const DocumentAuthProvider = ({
|
|||||||
documentAuthType={derivedRecipientActionAuth}
|
documentAuthType={derivedRecipientActionAuth}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</DocumentAuthContext.Provider>
|
</DocumentSigningAuthContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type ExecuteActionAuthProcedureOptions = Omit<
|
type ExecuteActionAuthProcedureOptions = Omit<
|
||||||
DocumentActionAuthDialogProps,
|
DocumentSigningAuthDialogProps,
|
||||||
'open' | 'onOpenChange' | 'documentAuthType' | 'recipientRole'
|
'open' | 'onOpenChange' | 'documentAuthType' | 'recipientRole'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
DocumentAuthProvider.displayName = 'DocumentAuthProvider';
|
DocumentSigningAuthProvider.displayName = 'DocumentSigningAuthProvider';
|
||||||
@ -1,19 +1,17 @@
|
|||||||
'use client';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useState, useTransition } from 'react';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Plural, Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Plural, Trans } from '@lingui/react/macro';
|
||||||
|
import type { Field, Recipient } from '@prisma/client';
|
||||||
|
import { FieldType } from '@prisma/client';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
import { P, match } from 'ts-pattern';
|
import { P, match } from 'ts-pattern';
|
||||||
|
|
||||||
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
|
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
|
||||||
import { DocumentAuth } from '@documenso/lib/types/document-auth';
|
import { DocumentAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
|
||||||
import { FieldType } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -27,10 +25,10 @@ import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/type
|
|||||||
import { Form } from '@documenso/ui/primitives/form/form';
|
import { Form } from '@documenso/ui/primitives/form/form';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { SigningDisclosure } from '~/components/general/signing-disclosure';
|
import { DocumentSigningDisclosure } from '~/components/general/document-signing/document-signing-disclosure';
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
import { useRequiredSigningContext } from './provider';
|
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||||
|
|
||||||
const AUTO_SIGNABLE_FIELD_TYPES: string[] = [
|
const AUTO_SIGNABLE_FIELD_TYPES: string[] = [
|
||||||
FieldType.NAME,
|
FieldType.NAME,
|
||||||
@ -55,22 +53,20 @@ const NON_AUTO_SIGNABLE_ACTION_AUTH_TYPES: string[] = [
|
|||||||
// while for larger documents with many fields it will be beneficial to sign away the boilerplate fields.
|
// while for larger documents with many fields it will be beneficial to sign away the boilerplate fields.
|
||||||
const AUTO_SIGN_THRESHOLD = 5;
|
const AUTO_SIGN_THRESHOLD = 5;
|
||||||
|
|
||||||
export type AutoSignProps = {
|
export type DocumentSigningAutoSignProps = {
|
||||||
recipient: Pick<Recipient, 'id' | 'token'>;
|
recipient: Pick<Recipient, 'id' | 'token'>;
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AutoSign = ({ recipient, fields }: AutoSignProps) => {
|
export const DocumentSigningAutoSign = ({ recipient, fields }: DocumentSigningAutoSignProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const router = useRouter();
|
const { email, fullName } = useRequiredDocumentSigningContext();
|
||||||
|
const { derivedRecipientActionAuth } = useRequiredDocumentSigningAuthContext();
|
||||||
const { email, fullName } = useRequiredSigningContext();
|
|
||||||
const { derivedRecipientActionAuth } = useRequiredDocumentAuthContext();
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
|
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
|
|
||||||
@ -158,11 +154,7 @@ export const AutoSign = ({ recipient, fields }: AutoSignProps) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
startTransition(() => {
|
await revalidate();
|
||||||
router.refresh();
|
|
||||||
|
|
||||||
setOpen(false);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
unsafe_useEffectOnce(() => {
|
unsafe_useEffectOnce(() => {
|
||||||
@ -205,7 +197,7 @@ export const AutoSign = ({ recipient, fields }: AutoSignProps) => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SigningDisclosure className="mt-4" />
|
<DocumentSigningDisclosure className="mt-4" />
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
@ -223,7 +215,7 @@ export const AutoSign = ({ recipient, fields }: AutoSignProps) => {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="min-w-[6rem]"
|
className="min-w-[6rem]"
|
||||||
loading={form.formState.isSubmitting || isPending}
|
loading={form.formState.isSubmitting}
|
||||||
disabled={!autoSignableFields.length}
|
disabled={!autoSignableFields.length}
|
||||||
>
|
>
|
||||||
<Trans>Sign</Trans>
|
<Trans>Sign</Trans>
|
||||||
@ -1,19 +1,16 @@
|
|||||||
'use client';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useEffect, useMemo, useState, useTransition } from 'react';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import type { Recipient } from '@prisma/client';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { ZCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
|
import { ZCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
|
||||||
import { fromCheckboxValue, toCheckboxValue } from '@documenso/lib/universal/field-checkbox';
|
import { fromCheckboxValue, toCheckboxValue } from '@documenso/lib/universal/field-checkbox';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
|
||||||
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type {
|
import type {
|
||||||
@ -26,28 +23,27 @@ import { checkboxValidationSigns } from '@documenso/ui/primitives/document-flow/
|
|||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
import { SigningFieldContainer } from './signing-field-container';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
|
|
||||||
export type CheckboxFieldProps = {
|
export type DocumentSigningCheckboxFieldProps = {
|
||||||
field: FieldWithSignatureAndFieldMeta;
|
field: FieldWithSignatureAndFieldMeta;
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CheckboxField = ({
|
export const DocumentSigningCheckboxField = ({
|
||||||
field,
|
field,
|
||||||
recipient,
|
recipient,
|
||||||
onSignField,
|
onSignField,
|
||||||
onUnsignField,
|
onUnsignField,
|
||||||
}: CheckboxFieldProps) => {
|
}: DocumentSigningCheckboxFieldProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const router = useRouter();
|
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
|
|
||||||
|
|
||||||
const parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
const parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
||||||
|
|
||||||
@ -89,7 +85,7 @@ export const CheckboxField = ({
|
|||||||
isPending: isRemoveSignedFieldWithTokenLoading,
|
isPending: isRemoveSignedFieldWithTokenLoading,
|
||||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
|
||||||
const shouldAutoSignField =
|
const shouldAutoSignField =
|
||||||
(!field.inserted && checkedValues.length > 0 && isLengthConditionMet) ||
|
(!field.inserted && checkedValues.length > 0 && isLengthConditionMet) ||
|
||||||
(!field.inserted && isReadOnly && isLengthConditionMet);
|
(!field.inserted && isReadOnly && isLengthConditionMet);
|
||||||
@ -110,7 +106,7 @@ export const CheckboxField = ({
|
|||||||
await signFieldWithToken(payload);
|
await signFieldWithToken(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -145,7 +141,7 @@ export const CheckboxField = ({
|
|||||||
setCheckedValues([]);
|
setCheckedValues([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
@ -217,7 +213,7 @@ export const CheckboxField = ({
|
|||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setCheckedValues(updatedValues);
|
setCheckedValues(updatedValues);
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -236,7 +232,12 @@ export const CheckboxField = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Checkbox">
|
<DocumentSigningFieldContainer
|
||||||
|
field={field}
|
||||||
|
onSign={onSign}
|
||||||
|
onRemove={onRemove}
|
||||||
|
type="Checkbox"
|
||||||
|
>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="bg-background absolute inset-0 z-20 flex items-center justify-center rounded-md">
|
<div className="bg-background absolute inset-0 z-20 flex items-center justify-center rounded-md">
|
||||||
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
||||||
@ -294,6 +295,6 @@ export const CheckboxField = ({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</SigningFieldContainer>
|
</DocumentSigningFieldContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1,10 +1,10 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Field } from '@prisma/client';
|
||||||
|
import { RecipientRole } from '@prisma/client';
|
||||||
|
|
||||||
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
|
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||||
import type { Field } from '@documenso/prisma/client';
|
|
||||||
import { RecipientRole } from '@documenso/prisma/client';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -14,9 +14,9 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
|
||||||
import { SigningDisclosure } from '~/components/general/signing-disclosure';
|
import { DocumentSigningDisclosure } from '~/components/general/document-signing/document-signing-disclosure';
|
||||||
|
|
||||||
export type SignDialogProps = {
|
export type DocumentSigningCompleteDialogProps = {
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
documentTitle: string;
|
documentTitle: string;
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
@ -26,7 +26,7 @@ export type SignDialogProps = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SignDialog = ({
|
export const DocumentSigningCompleteDialog = ({
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
documentTitle,
|
documentTitle,
|
||||||
fields,
|
fields,
|
||||||
@ -34,7 +34,7 @@ export const SignDialog = ({
|
|||||||
onSignatureComplete,
|
onSignatureComplete,
|
||||||
role,
|
role,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}: SignDialogProps) => {
|
}: DocumentSigningCompleteDialogProps) => {
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
|
|
||||||
const isComplete = useMemo(() => !fieldsContainUnsignedRequiredField(fields), [fields]);
|
const isComplete = useMemo(() => !fieldsContainUnsignedRequiredField(fields), [fields]);
|
||||||
@ -116,7 +116,7 @@ export const SignDialog = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SigningDisclosure className="mt-4" />
|
<DocumentSigningDisclosure className="mt-4" />
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
||||||
@ -1,12 +1,9 @@
|
|||||||
'use client';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { useTransition } from 'react';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Recipient } from '@prisma/client';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_DOCUMENT_DATE_FORMAT,
|
DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||||
@ -16,7 +13,6 @@ import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'
|
|||||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
|
||||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type {
|
import type {
|
||||||
@ -25,9 +21,9 @@ import type {
|
|||||||
} from '@documenso/trpc/server/field-router/schema';
|
} from '@documenso/trpc/server/field-router/schema';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { SigningFieldContainer } from './signing-field-container';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
|
|
||||||
export type DateFieldProps = {
|
export type DocumentSigningDateFieldProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
dateFormat?: string | null;
|
dateFormat?: string | null;
|
||||||
@ -36,20 +32,17 @@ export type DateFieldProps = {
|
|||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DateField = ({
|
export const DocumentSigningDateField = ({
|
||||||
field,
|
field,
|
||||||
recipient,
|
recipient,
|
||||||
dateFormat = DEFAULT_DOCUMENT_DATE_FORMAT,
|
dateFormat = DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||||
timezone = DEFAULT_DOCUMENT_TIME_ZONE,
|
timezone = DEFAULT_DOCUMENT_TIME_ZONE,
|
||||||
onSignField,
|
onSignField,
|
||||||
onUnsignField,
|
onUnsignField,
|
||||||
}: DateFieldProps) => {
|
}: DocumentSigningDateFieldProps) => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
|
|
||||||
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
||||||
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
@ -59,14 +52,14 @@ export const DateField = ({
|
|||||||
isPending: isRemoveSignedFieldWithTokenLoading,
|
isPending: isRemoveSignedFieldWithTokenLoading,
|
||||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
|
||||||
|
|
||||||
const localDateString = convertToLocalSystemFormat(field.customText, dateFormat, timezone);
|
const localDateString = convertToLocalSystemFormat(field.customText, dateFormat, timezone);
|
||||||
|
|
||||||
const isDifferentTime = field.inserted && localDateString !== field.customText;
|
const isDifferentTime = field.inserted && localDateString !== field.customText;
|
||||||
|
|
||||||
const tooltipText = _(
|
const tooltipText = _(
|
||||||
msg`"${field.customText}" will appear on the document as it has a timezone of "${timezone}".`,
|
msg`"${field.customText}" will appear on the document as it has a timezone of "${timezone || ''}".`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
||||||
@ -85,7 +78,7 @@ export const DateField = ({
|
|||||||
|
|
||||||
await signFieldWithToken(payload);
|
await signFieldWithToken(payload);
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -117,7 +110,7 @@ export const DateField = ({
|
|||||||
|
|
||||||
await removeSignedFieldWithToken(payload);
|
await removeSignedFieldWithToken(payload);
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
@ -130,7 +123,7 @@ export const DateField = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigningFieldContainer
|
<DocumentSigningFieldContainer
|
||||||
field={field}
|
field={field}
|
||||||
onSign={onSign}
|
onSign={onSign}
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
@ -154,6 +147,6 @@ export const DateField = ({
|
|||||||
{localDateString}
|
{localDateString}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</SigningFieldContainer>
|
</DocumentSigningFieldContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1,14 +1,16 @@
|
|||||||
import type { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { Link } from 'react-router';
|
||||||
import { Trans } from '@lingui/macro';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
export type SigningDisclosureProps = HTMLAttributes<HTMLParagraphElement>;
|
export type DocumentSigningDisclosureProps = HTMLAttributes<HTMLParagraphElement>;
|
||||||
|
|
||||||
export const SigningDisclosure = ({ className, ...props }: SigningDisclosureProps) => {
|
export const DocumentSigningDisclosure = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: DocumentSigningDisclosureProps) => {
|
||||||
return (
|
return (
|
||||||
<p className={cn('text-muted-foreground text-xs', className)} {...props}>
|
<p className={cn('text-muted-foreground text-xs', className)} {...props}>
|
||||||
<Trans>
|
<Trans>
|
||||||
@ -22,7 +24,7 @@ export const SigningDisclosure = ({ className, ...props }: SigningDisclosureProp
|
|||||||
Read the full{' '}
|
Read the full{' '}
|
||||||
<Link
|
<Link
|
||||||
className="text-documenso-700 underline"
|
className="text-documenso-700 underline"
|
||||||
href="/articles/signature-disclosure"
|
to="/articles/signature-disclosure"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
signature disclosure
|
signature disclosure
|
||||||
@ -1,18 +1,15 @@
|
|||||||
'use client';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useEffect, useState, useTransition } from 'react';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import type { Recipient } from '@prisma/client';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { ZDropdownFieldMeta } from '@documenso/lib/types/field-meta';
|
import { ZDropdownFieldMeta } from '@documenso/lib/types/field-meta';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
|
||||||
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type {
|
import type {
|
||||||
@ -29,29 +26,27 @@ import {
|
|||||||
} from '@documenso/ui/primitives/select';
|
} from '@documenso/ui/primitives/select';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
import { SigningFieldContainer } from './signing-field-container';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
|
|
||||||
export type DropdownFieldProps = {
|
export type DocumentSigningDropdownFieldProps = {
|
||||||
field: FieldWithSignatureAndFieldMeta;
|
field: FieldWithSignatureAndFieldMeta;
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DropdownField = ({
|
export const DocumentSigningDropdownField = ({
|
||||||
field,
|
field,
|
||||||
recipient,
|
recipient,
|
||||||
onSignField,
|
onSignField,
|
||||||
onUnsignField,
|
onUnsignField,
|
||||||
}: DropdownFieldProps) => {
|
}: DocumentSigningDropdownFieldProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const router = useRouter();
|
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
|
|
||||||
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
|
|
||||||
|
|
||||||
const parsedFieldMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
|
const parsedFieldMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
|
||||||
const isReadOnly = parsedFieldMeta?.readOnly;
|
const isReadOnly = parsedFieldMeta?.readOnly;
|
||||||
@ -66,7 +61,7 @@ export const DropdownField = ({
|
|||||||
isPending: isRemoveSignedFieldWithTokenLoading,
|
isPending: isRemoveSignedFieldWithTokenLoading,
|
||||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
|
||||||
const shouldAutoSignField =
|
const shouldAutoSignField =
|
||||||
(!field.inserted && localChoice) || (!field.inserted && isReadOnly && defaultValue);
|
(!field.inserted && localChoice) || (!field.inserted && isReadOnly && defaultValue);
|
||||||
|
|
||||||
@ -91,7 +86,8 @@ export const DropdownField = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLocalChoice('');
|
setLocalChoice('');
|
||||||
startTransition(() => router.refresh());
|
|
||||||
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -128,7 +124,8 @@ export const DropdownField = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLocalChoice('');
|
setLocalChoice('');
|
||||||
startTransition(() => router.refresh());
|
|
||||||
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
@ -164,7 +161,7 @@ export const DropdownField = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pointer-events-none">
|
<div className="pointer-events-none">
|
||||||
<SigningFieldContainer
|
<DocumentSigningFieldContainer
|
||||||
field={field}
|
field={field}
|
||||||
onPreSign={onPreSign}
|
onPreSign={onPreSign}
|
||||||
onSign={onSign}
|
onSign={onSign}
|
||||||
@ -210,7 +207,7 @@ export const DropdownField = ({
|
|||||||
{field.customText}
|
{field.customText}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</SigningFieldContainer>
|
</DocumentSigningFieldContainer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1,17 +1,13 @@
|
|||||||
'use client';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { useTransition } from 'react';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Recipient } from '@prisma/client';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
|
||||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type {
|
import type {
|
||||||
@ -20,25 +16,27 @@ import type {
|
|||||||
} from '@documenso/trpc/server/field-router/schema';
|
} from '@documenso/trpc/server/field-router/schema';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { useRequiredSigningContext } from './provider';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
import { SigningFieldContainer } from './signing-field-container';
|
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||||
|
|
||||||
export type EmailFieldProps = {
|
export type DocumentSigningEmailFieldProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EmailField = ({ field, recipient, onSignField, onUnsignField }: EmailFieldProps) => {
|
export const DocumentSigningEmailField = ({
|
||||||
const router = useRouter();
|
field,
|
||||||
|
recipient,
|
||||||
|
onSignField,
|
||||||
|
onUnsignField,
|
||||||
|
}: DocumentSigningEmailFieldProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const { email: providedEmail } = useRequiredSigningContext();
|
const { email: providedEmail } = useRequiredDocumentSigningContext();
|
||||||
|
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
|
|
||||||
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
||||||
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
@ -48,7 +46,7 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
|
|||||||
isPending: isRemoveSignedFieldWithTokenLoading,
|
isPending: isRemoveSignedFieldWithTokenLoading,
|
||||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
|
||||||
|
|
||||||
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
||||||
try {
|
try {
|
||||||
@ -69,7 +67,7 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
|
|||||||
|
|
||||||
await signFieldWithToken(payload);
|
await signFieldWithToken(payload);
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -101,7 +99,7 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
|
|||||||
|
|
||||||
await removeSignedFieldWithToken(payload);
|
await removeSignedFieldWithToken(payload);
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
@ -114,7 +112,7 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Email">
|
<DocumentSigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Email">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
||||||
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
||||||
@ -132,6 +130,6 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
|
|||||||
{field.customText}
|
{field.customText}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</SigningFieldContainer>
|
</DocumentSigningFieldContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1,21 +1,19 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { FieldType } from '@prisma/client';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
import { type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import { type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
import { FieldType } from '@documenso/prisma/client';
|
|
||||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
import { FieldRootContainer } from '@documenso/ui/components/field/field';
|
import { FieldRootContainer } from '@documenso/ui/components/field/field';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
|
|
||||||
export type SignatureFieldProps = {
|
export type DocumentSigningFieldContainerProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -53,7 +51,7 @@ export type SignatureFieldProps = {
|
|||||||
tooltipText?: string | null;
|
tooltipText?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SigningFieldContainer = ({
|
export const DocumentSigningFieldContainer = ({
|
||||||
field,
|
field,
|
||||||
loading,
|
loading,
|
||||||
onPreSign,
|
onPreSign,
|
||||||
@ -62,8 +60,9 @@ export const SigningFieldContainer = ({
|
|||||||
children,
|
children,
|
||||||
type,
|
type,
|
||||||
tooltipText,
|
tooltipText,
|
||||||
}: SignatureFieldProps) => {
|
}: DocumentSigningFieldContainerProps) => {
|
||||||
const { executeActionAuthProcedure, isAuthRedirectRequired } = useRequiredDocumentAuthContext();
|
const { executeActionAuthProcedure, isAuthRedirectRequired } =
|
||||||
|
useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
const parsedFieldMeta = field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined;
|
const parsedFieldMeta = field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined;
|
||||||
const readOnlyField = parsedFieldMeta?.readOnly || false;
|
const readOnlyField = parsedFieldMeta?.readOnly || false;
|
||||||
@ -1,19 +1,16 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { type Field, FieldType, type Recipient, RecipientRole } from '@prisma/client';
|
||||||
import { Trans } from '@lingui/macro';
|
|
||||||
import { useSession } from 'next-auth/react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||||
|
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
|
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import { type Field, FieldType, type Recipient, RecipientRole } from '@documenso/prisma/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@ -23,10 +20,10 @@ import { Input } from '@documenso/ui/primitives/input';
|
|||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
|
|
||||||
import { useRequiredSigningContext } from './provider';
|
import { DocumentSigningCompleteDialog } from './document-signing-complete-dialog';
|
||||||
import { SignDialog } from './sign-dialog';
|
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||||
|
|
||||||
export type SigningFormProps = {
|
export type DocumentSigningFormProps = {
|
||||||
document: DocumentAndSender;
|
document: DocumentAndSender;
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
@ -34,19 +31,20 @@ export type SigningFormProps = {
|
|||||||
isRecipientsTurn: boolean;
|
isRecipientsTurn: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SigningForm = ({
|
export const DocumentSigningForm = ({
|
||||||
document,
|
document,
|
||||||
recipient,
|
recipient,
|
||||||
fields,
|
fields,
|
||||||
redirectUrl,
|
redirectUrl,
|
||||||
isRecipientsTurn,
|
isRecipientsTurn,
|
||||||
}: SigningFormProps) => {
|
}: DocumentSigningFormProps) => {
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const analytics = useAnalytics();
|
const analytics = useAnalytics();
|
||||||
const { data: session } = useSession();
|
|
||||||
|
const { user } = useOptionalSession();
|
||||||
|
|
||||||
const { fullName, signature, setFullName, setSignature, signatureValid, setSignatureValid } =
|
const { fullName, signature, setFullName, setSignature, signatureValid, setSignatureValid } =
|
||||||
useRequiredSigningContext();
|
useRequiredDocumentSigningContext();
|
||||||
|
|
||||||
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||||
|
|
||||||
@ -109,7 +107,11 @@ export const SigningForm = ({
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
redirectUrl ? router.push(redirectUrl) : router.push(`/sign/${recipient.token}/complete`);
|
if (redirectUrl) {
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
} else {
|
||||||
|
await navigate(`/sign/${recipient.token}/complete`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -117,8 +119,8 @@ export const SigningForm = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'dark:bg-background border-border bg-widget sticky flex h-full flex-col rounded-xl border px-4 py-6',
|
'dark:bg-background border-border bg-widget sticky flex h-full flex-col rounded-xl border px-4 py-6',
|
||||||
{
|
{
|
||||||
'top-20 max-h-[min(68rem,calc(100vh-6rem))]': session,
|
'top-20 max-h-[min(68rem,calc(100vh-6rem))]': user,
|
||||||
'top-4 max-h-[min(68rem,calc(100vh-2rem))]': !session,
|
'top-4 max-h-[min(68rem,calc(100vh-2rem))]': !user,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
@ -159,12 +161,12 @@ export const SigningForm = ({
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||||
onClick={() => router.back()}
|
onClick={async () => navigate(-1)}
|
||||||
>
|
>
|
||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<SignDialog
|
<DocumentSigningCompleteDialog
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
onSignatureComplete={handleSubmit(onFormSubmit)}
|
onSignatureComplete={handleSubmit(onFormSubmit)}
|
||||||
documentTitle={document.title}
|
documentTitle={document.title}
|
||||||
@ -241,12 +243,12 @@ export const SigningForm = ({
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||||
onClick={() => router.back()}
|
onClick={async () => navigate(-1)}
|
||||||
>
|
>
|
||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<SignDialog
|
<DocumentSigningCompleteDialog
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
onSignatureComplete={handleSubmit(onFormSubmit)}
|
onSignatureComplete={handleSubmit(onFormSubmit)}
|
||||||
documentTitle={document.title}
|
documentTitle={document.title}
|
||||||
@ -1,18 +1,14 @@
|
|||||||
'use client';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { useTransition } from 'react';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Recipient } from '@prisma/client';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
|
||||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type {
|
import type {
|
||||||
@ -21,31 +17,29 @@ import type {
|
|||||||
} from '@documenso/trpc/server/field-router/schema';
|
} from '@documenso/trpc/server/field-router/schema';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { useRequiredSigningContext } from './provider';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
import { SigningFieldContainer } from './signing-field-container';
|
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||||
|
|
||||||
export type InitialsFieldProps = {
|
export type DocumentSigningInitialsFieldProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InitialsField = ({
|
export const DocumentSigningInitialsField = ({
|
||||||
field,
|
field,
|
||||||
recipient,
|
recipient,
|
||||||
onSignField,
|
onSignField,
|
||||||
onUnsignField,
|
onUnsignField,
|
||||||
}: InitialsFieldProps) => {
|
}: DocumentSigningInitialsFieldProps) => {
|
||||||
const router = useRouter();
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const { fullName } = useRequiredSigningContext();
|
const { fullName } = useRequiredDocumentSigningContext();
|
||||||
const initials = extractInitials(fullName);
|
const initials = extractInitials(fullName);
|
||||||
|
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
|
|
||||||
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
||||||
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
@ -54,7 +48,7 @@ export const InitialsField = ({
|
|||||||
isPending: isRemoveSignedFieldWithTokenLoading,
|
isPending: isRemoveSignedFieldWithTokenLoading,
|
||||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
|
||||||
|
|
||||||
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
||||||
try {
|
try {
|
||||||
@ -75,7 +69,7 @@ export const InitialsField = ({
|
|||||||
|
|
||||||
await signFieldWithToken(payload);
|
await signFieldWithToken(payload);
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -107,7 +101,7 @@ export const InitialsField = ({
|
|||||||
|
|
||||||
await removeSignedFieldWithToken(payload);
|
await removeSignedFieldWithToken(payload);
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
@ -120,7 +114,12 @@ export const InitialsField = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Initials">
|
<DocumentSigningFieldContainer
|
||||||
|
field={field}
|
||||||
|
onSign={onSign}
|
||||||
|
onRemove={onRemove}
|
||||||
|
type="Initials"
|
||||||
|
>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
||||||
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
||||||
@ -138,6 +137,6 @@ export const InitialsField = ({
|
|||||||
{field.customText}
|
{field.customText}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</SigningFieldContainer>
|
</DocumentSigningFieldContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1,17 +1,15 @@
|
|||||||
'use client';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useState, useTransition } from 'react';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { type Recipient } from '@prisma/client';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { type Recipient } from '@documenso/prisma/client';
|
|
||||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type {
|
import type {
|
||||||
@ -24,29 +22,31 @@ import { Input } from '@documenso/ui/primitives/input';
|
|||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
import { useRequiredSigningContext } from './provider';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
import { SigningFieldContainer } from './signing-field-container';
|
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||||
|
|
||||||
export type NameFieldProps = {
|
export type DocumentSigningNameFieldProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NameField = ({ field, recipient, onSignField, onUnsignField }: NameFieldProps) => {
|
export const DocumentSigningNameField = ({
|
||||||
const router = useRouter();
|
field,
|
||||||
|
recipient,
|
||||||
|
onSignField,
|
||||||
|
onUnsignField,
|
||||||
|
}: DocumentSigningNameFieldProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const { fullName: providedFullName, setFullName: setProvidedFullName } =
|
const { fullName: providedFullName, setFullName: setProvidedFullName } =
|
||||||
useRequiredSigningContext();
|
useRequiredDocumentSigningContext();
|
||||||
|
|
||||||
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
|
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
|
|
||||||
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
||||||
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
@ -56,7 +56,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
|
|||||||
isPending: isRemoveSignedFieldWithTokenLoading,
|
isPending: isRemoveSignedFieldWithTokenLoading,
|
||||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
|
||||||
|
|
||||||
const [showFullNameModal, setShowFullNameModal] = useState(false);
|
const [showFullNameModal, setShowFullNameModal] = useState(false);
|
||||||
const [localFullName, setLocalFullName] = useState('');
|
const [localFullName, setLocalFullName] = useState('');
|
||||||
@ -107,7 +107,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
|
|||||||
|
|
||||||
await signFieldWithToken(payload);
|
await signFieldWithToken(payload);
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -139,7 +139,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
|
|||||||
|
|
||||||
await removeSignedFieldWithToken(payload);
|
await removeSignedFieldWithToken(payload);
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
@ -152,7 +152,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigningFieldContainer
|
<DocumentSigningFieldContainer
|
||||||
field={field}
|
field={field}
|
||||||
onPreSign={onPreSign}
|
onPreSign={onPreSign}
|
||||||
onSign={onSign}
|
onSign={onSign}
|
||||||
@ -227,6 +227,6 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</SigningFieldContainer>
|
</DocumentSigningFieldContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1,19 +1,17 @@
|
|||||||
'use client';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useEffect, useState, useTransition } from 'react';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { Recipient } from '@prisma/client';
|
||||||
import { Hash, Loader } from 'lucide-react';
|
import { Hash, Loader } from 'lucide-react';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
|
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
|
||||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { ZNumberFieldMeta } from '@documenso/lib/types/field-meta';
|
import { ZNumberFieldMeta } from '@documenso/lib/types/field-meta';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
|
||||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type {
|
import type {
|
||||||
@ -26,8 +24,8 @@ import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
import { SigningFieldContainer } from './signing-field-container';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
|
|
||||||
type ValidationErrors = {
|
type ValidationErrors = {
|
||||||
isNumber: string[];
|
isNumber: string[];
|
||||||
@ -37,19 +35,23 @@ type ValidationErrors = {
|
|||||||
numberFormat: string[];
|
numberFormat: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NumberFieldProps = {
|
export type DocumentSigningNumberFieldProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NumberField = ({ field, recipient, onSignField, onUnsignField }: NumberFieldProps) => {
|
export const DocumentSigningNumberField = ({
|
||||||
|
field,
|
||||||
|
recipient,
|
||||||
|
onSignField,
|
||||||
|
onUnsignField,
|
||||||
|
}: DocumentSigningNumberFieldProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
const [showRadioModal, setShowRadioModal] = useState(false);
|
const [showRadioModal, setShowRadioModal] = useState(false);
|
||||||
|
|
||||||
const parsedFieldMeta = field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null;
|
const parsedFieldMeta = field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null;
|
||||||
@ -69,7 +71,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
|||||||
|
|
||||||
const [errors, setErrors] = useState(initialErrors);
|
const [errors, setErrors] = useState(initialErrors);
|
||||||
|
|
||||||
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
|
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
||||||
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
@ -79,7 +81,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
|||||||
isPending: isRemoveSignedFieldWithTokenLoading,
|
isPending: isRemoveSignedFieldWithTokenLoading,
|
||||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
|
||||||
|
|
||||||
const handleNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const text = e.target.value;
|
const text = e.target.value;
|
||||||
@ -135,7 +137,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
|||||||
|
|
||||||
setLocalNumber('');
|
setLocalNumber('');
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -186,7 +188,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
|||||||
|
|
||||||
setLocalNumber(parsedFieldMeta?.value ? String(parsedFieldMeta?.value) : '');
|
setLocalNumber(parsedFieldMeta?.value ? String(parsedFieldMeta?.value) : '');
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
await revalidate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
@ -229,7 +231,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
|||||||
const userInputHasErrors = Object.values(errors).some((error) => error.length > 0);
|
const userInputHasErrors = Object.values(errors).some((error) => error.length > 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigningFieldContainer
|
<DocumentSigningFieldContainer
|
||||||
field={field}
|
field={field}
|
||||||
onPreSign={onPreSign}
|
onPreSign={onPreSign}
|
||||||
onSign={onSign}
|
onSign={onSign}
|
||||||
@ -340,6 +342,6 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</SigningFieldContainer>
|
</DocumentSigningFieldContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user