Compare commits

..

26 Commits

Author SHA1 Message Date
Mythie
dc36a8182c v1.9.0-rc.11 2025-01-21 09:49:22 +11:00
Mythie
0ef85b47b1 fix: handle empty object as fieldMeta 2025-01-21 09:46:54 +11:00
Mythie
058d9dd0ba v1.9.0-rc.10 2025-01-20 19:54:39 +11:00
David Nguyen
74bb230247 fix: add empty success responses (#1600) 2025-01-20 19:47:39 +11:00
Mythie
7c1e0f34e8 v1.9.0-rc.9 2025-01-20 16:08:15 +11:00
Mythie
7e31323faa fix: add team context to more vanilla client usages 2025-01-20 15:53:28 +11:00
David Nguyen
a28cdf437b feat: add get field endpoints (#1599) 2025-01-20 15:53:12 +11:00
Ephraim Duncan
80dfbeb16f feat: add angular embedding docs (#1592) 2025-01-20 09:35:47 +11:00
Mythie
9de3a32ceb fix: pass team id to vanilla trpc client 2025-01-20 09:30:36 +11:00
David Nguyen
0d3864548c fix: bump trpc and openapi packages (#1591) 2025-01-19 22:07:02 +11:00
David Nguyen
9e03747e43 feat: add create document beta endpoint (#1584) 2025-01-16 13:36:00 +11:00
David Nguyen
5750f2b477 feat: add prisma json types (#1583) 2025-01-15 13:46:45 +11:00
David Nguyen
901be70f97 feat: add consistent response schemas (#1582) 2025-01-14 00:43:35 +11:00
David Nguyen
7d0a9c6439 fix: refactor prisma relations (#1581) 2025-01-13 13:41:53 +11:00
Samuel Raub
48b55758e3 feat: ignore unrecognized fields from authorization response (#1479)
When authenticating using OIDC some IDPs send additional fields in their
authorization response. 

This leads to an error because these fields can't be persisted to the DB 
through the auth.js prisma adapter. 

This PR solves this by deleting all unrecognized fields from the 
authorization response before persisting. 

This behaviour is also compliant to 
[RFC6749 Section 4.1.2](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2)
2025-01-13 11:05:37 +11:00
Mythie
dcaccb65f2 v1.9.0-rc.8 2025-01-13 10:21:05 +11:00
Mythie
723e1b4ea2 fix: include all template meta in findTemplates 2025-01-13 09:34:23 +11:00
David Nguyen
08a69c6168 fix: pending document edit (#1580)
Fix issue where you cannot edit a pending document when there is a CCer
recipient.
2025-01-11 22:31:59 +11:00
David Nguyen
948d9c24cf fix: broken direct template webhook (#1579) 2025-01-11 21:42:33 +11:00
David Nguyen
ebbe922982 feat: add template and field endpoints (#1572) 2025-01-11 15:33:20 +11:00
Timur Ercan
6520bbd5e3 Update FEATURES
chore: typo
2025-01-10 14:25:39 +01:00
Mythie
4e197ac24c v1.9.0-rc.7 2025-01-09 15:07:11 +11:00
Ephraim Duncan
f707e5fb10 fix: update template field schema (#1575)
Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
2025-01-09 12:06:17 +11:00
Catalin Pit
6fc5e565d0 fix: add document visibility to template (#1566)
Adds the visibility property to templates
2025-01-09 10:14:24 +11:00
Mythie
07c852744b v1.9.0-rc.6 2025-01-08 20:18:09 +11:00
Lucas Smith
4fab98c633 feat: allow switching document when using templates (#1571)
Adds the ability to upload a custom document when using a template.

This is useful when you have a given fixed template with placeholder
values that you want to decorate with Documenso fields but will then
create a final specialised document when sending it out to a given
recipient.
2025-01-07 16:13:35 +11:00
331 changed files with 11169 additions and 10508 deletions

View File

@@ -10,7 +10,7 @@ on:
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze
runs-on: ubuntu-latest runs-on: ubuntu-22.04
permissions: permissions:
actions: read actions: read
contents: read contents: read

View File

@@ -8,7 +8,7 @@ jobs:
e2e_tests: e2e_tests:
name: 'E2E Tests' name: 'E2E Tests'
timeout-minutes: 60 timeout-minutes: 60
runs-on: ubuntu-22.04 runs-on: warp-ubuntu-2204-x64-16x
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View File

@@ -27,6 +27,6 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"typescript": "^5" "typescript": "5.6.2"
} }
} }

View File

@@ -5,5 +5,6 @@
"svelte": "Svelte Integration", "svelte": "Svelte Integration",
"solid": "Solid Integration", "solid": "Solid Integration",
"preact": "Preact Integration", "preact": "Preact Integration",
"angular": "Angular Integration",
"css-variables": "CSS Variables" "css-variables": "CSS Variables"
} }

View File

@@ -0,0 +1,90 @@
---
title: Angular Integration
description: Learn how to use our embedding SDK within your Angular application.
---
# Angular Integration
Our Angular SDK provides a simple way to embed a signing experience within your Angular application. It supports both direct link templates and signing tokens.
## Installation
To install the SDK, run the following command:
```bash
npm install @documenso/embed-angular
```
## Usage
To embed a signing experience, you'll need to provide the token for the document you want to embed. This can be done in a few different ways, depending on your use case.
### Direct Link Template
If you have a direct link template, you can simply provide the token for the template to the `EmbedDirectTemplate` component.
```typescript
import { Component } from '@angular/core';
import { EmbedDirectTemplate } from '@documenso/embed-angular';
@Component({
selector: 'app-embedding',
template: `
<embed-direct-template [token]="token" />
`,
standalone: true,
imports: [EmbedDirectTemplate],
})
export class EmbeddingComponent {
token = 'YOUR_TOKEN_HERE'; // Replace with the actual token
}
```
#### Props
| Prop | Type | Description |
| ------------------- | ------------------- | ------------------------------------------------------------------------------------------ |
| token | string | The token for the document you want to embed |
| host | string (optional) | The host to be used for the signing experience, relevant for self-hosters |
| name | string (optional) | The name the signer that will be used by default for signing |
| lockName | boolean (optional) | Whether or not the name field should be locked disallowing modifications |
| email | string (optional) | The email the signer that will be used by default for signing |
| lockEmail | boolean (optional) | Whether or not the email field should be locked disallowing modifications |
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |
| onFieldSigned | function (optional) | A callback function that will be called when a field is signed |
| onFieldUnsigned | function (optional) | A callback function that will be called when a field is unsigned |
### Signing Token
If you have a signing token, you can provide it to the `EmbedSignDocument` component.
```typescript
import { Component } from '@angular/core';
import { EmbedSignDocument } from '@documenso/embed-angular';
@Component({
selector: 'app-embedding',
template: `
<embed-sign-document [token]="token" />
`,
standalone: true,
imports: [EmbedSignDocument],
})
export class EmbeddingComponent {
token = 'YOUR_TOKEN_HERE'; // Replace with the actual token
}
```
#### Props
| Prop | Type | Description |
| ------------------- | ------------------- | ------------------------------------------------------------------------------------------ |
| token | string | The token for the document you want to embed |
| host | string (optional) | The host to be used for the signing experience, relevant for self-hosters |
| name | string (optional) | The name the signer that will be used by default for signing |
| lockName | boolean (optional) | Whether or not the name field should be locked disallowing modifications |
| onDocumentReady | function (optional) | A callback function that will be called when the document is loaded and ready to be signed |
| onDocumentCompleted | function (optional) | A callback function that will be called when the document has been completed |
| onDocumentError | function (optional) | A callback function that will be called when an error occurs with the document |

View File

@@ -5,7 +5,7 @@ description: Learn how to use embedding to bring signing to your own website or
# Embedding # Embedding
Our embedding feature lets you integrate our document signing experience into your own application or website. Whether you're building with React, Preact, Vue, Svelte, Solid, or using generalized web components, this guide will help you get started with embedding Documenso. Our embedding feature lets you integrate our document signing experience into your own application or website. Whether you're building with React, Preact, Vue, Svelte, Solid, Angular, or using generalized web components, this guide will help you get started with embedding Documenso.
## Availability ## Availability
@@ -74,12 +74,13 @@ These customization options are available for both Direct Templates and Signing
We support embedding across a range of popular JavaScript frameworks, including: We support embedding across a range of popular JavaScript frameworks, including:
| Framework | Package | | Framework | Package |
| --------- | -------------------------------------------------------------------------------- | | --------- | ---------------------------------------------------------------------------------- |
| React | [@documenso/embed-react](https://www.npmjs.com/package/@documenso/embed-react) | | React | [@documenso/embed-react](https://www.npmjs.com/package/@documenso/embed-react) |
| Preact | [@documenso/embed-preact](https://www.npmjs.com/package/@documenso/embed-preact) | | Preact | [@documenso/embed-preact](https://www.npmjs.com/package/@documenso/embed-preact) |
| Vue | [@documenso/embed-vue](https://www.npmjs.com/package/@documenso/embed-vue) | | Vue | [@documenso/embed-vue](https://www.npmjs.com/package/@documenso/embed-vue) |
| Svelte | [@documenso/embed-svelte](https://www.npmjs.com/package/@documenso/embed-svelte) | | Svelte | [@documenso/embed-svelte](https://www.npmjs.com/package/@documenso/embed-svelte) |
| Solid | [@documenso/embed-solid](https://www.npmjs.com/package/@documenso/embed-solid) | | Solid | [@documenso/embed-solid](https://www.npmjs.com/package/@documenso/embed-solid) |
| Angular | [@documenso/embed-angular](https://www.npmjs.com/package/@documenso/embed-angular) |
Additionally, we provide **web components** for more generalized use. However, please note that web components are still in their early stages and haven't been extensively tested. Additionally, we provide **web components** for more generalized use. However, please note that web components are still in their early stages and haven't been extensively tested.
@@ -127,7 +128,7 @@ This will show a dialog which will ask you to configure which recipient should b
## Embedding with Signing Tokens ## Embedding with Signing Tokens
To embed the signing process for an ordinary document, youll need a **document signing token** for the recipient. This token provides the necessary access to load the document and facilitate the signing process securely. To embed the signing process for an ordinary document, you'll need a **document signing token** for the recipient. This token provides the necessary access to load the document and facilitate the signing process securely.
#### Instructions #### Instructions
@@ -164,6 +165,7 @@ Once you've obtained the appropriate tokens, you can integrate the signing exper
- [Vue](/developers/embedding/vue) - [Vue](/developers/embedding/vue)
- [Svelte](/developers/embedding/svelte) - [Svelte](/developers/embedding/svelte)
- [Solid](/developers/embedding/solid) - [Solid](/developers/embedding/solid)
- [Angular](/developers/embedding/angular)
If you're using **web components**, the integration process is slightly different. Keep in mind that web components are currently less tested but can still provide flexibility for general use cases. If you're using **web components**, the integration process is slightly different. Keep in mind that web components are currently less tested but can still provide flexibility for general use cases.
@@ -174,4 +176,5 @@ If you're using **web components**, the integration process is slightly differen
- [Svelte Integration](/developers/embedding/svelte) - [Svelte Integration](/developers/embedding/svelte)
- [Solid Integration](/developers/embedding/solid) - [Solid Integration](/developers/embedding/solid)
- [Preact Integration](/developers/embedding/preact) - [Preact Integration](/developers/embedding/preact)
- [Angular Integration](/developers/embedding/angular)
- [CSS Variables](/developers/embedding/css-variables) - [CSS Variables](/developers/embedding/css-variables)

View File

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

View File

@@ -1,4 +0,0 @@
.react-router
build
node_modules
README.md

View File

@@ -1,9 +0,0 @@
.DS_Store
/node_modules/
# React Router
/.react-router/
/build/
# Vite
vite.config.ts.timestamp*

View File

@@ -1,22 +0,0 @@
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"]

View File

@@ -1,25 +0,0 @@
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"]

View File

@@ -1,26 +0,0 @@
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"]

View File

@@ -1,100 +0,0 @@
# Welcome to React Router!
A modern, production-ready template for building full-stack React applications using React Router.
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](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.

View File

@@ -1 +0,0 @@
@import '@documenso/ui/styles/theme.css';

View File

@@ -1,13 +0,0 @@
import { twoFactor } from 'better-auth/plugins';
import { createAuthClient } from 'better-auth/react';
import { passkeyClientPlugin } from './auth/passkey-plugin/client';
// make sure to import from better-auth/react
export const authClient = createAuthClient({
baseURL: 'http://localhost:3000',
plugins: [twoFactor(), passkeyClientPlugin()],
});
export const { signIn, signOut, useSession } = authClient;

View File

@@ -1,112 +0,0 @@
import { compare, hash } from '@node-rs/bcrypt';
import { betterAuth } from 'better-auth';
import { prismaAdapter } from 'better-auth/adapters/prisma';
import { twoFactor } from 'better-auth/plugins';
import { getAuthenticatorOptions } from '@documenso/lib/utils/authenticator';
import { prisma } from '@documenso/prisma';
import { passkeyPlugin } from './auth/passkey-plugin';
// todo: import from @documenso/lib/constants/auth
export const SALT_ROUNDS = 12;
const passkeyOptions = getAuthenticatorOptions();
export const auth = betterAuth({
appName: 'Documenso',
plugins: [
twoFactor({
issuer: 'Documenso',
skipVerificationOnEnable: true,
// totpOptions: {
// },
schema: {
twoFactor: {
modelName: 'TwoFactor',
fields: {
userId: 'userId',
secret: 'secret',
backupCodes: 'backupCodes',
},
},
},
// todo: add options
}),
passkeyPlugin(),
// passkey({
// rpID: passkeyOptions.rpId,
// rpName: passkeyOptions.rpName,
// origin: passkeyOptions.origin,
// schema: {
// passkey: {
// fields: {
// publicKey: 'credentialPublicKey',
// credentialID: 'credentialId',
// deviceType: 'credentialDeviceType',
// backedUp: 'credentialBackedUp',
// // transports: '',
// },
// },
// },
// }),
],
secret: 'secret', // todo
database: prismaAdapter(prisma, {
provider: 'postgresql',
}),
databaseHooks: {
account: {
create: {
before: (session) => {
return {
data: {
...session,
accountId: session.accountId.toString(),
},
};
},
},
},
},
session: {
fields: {
token: 'sessionToken',
expiresAt: 'expires',
},
},
user: {
fields: {
emailVerified: 'isEmailVerified',
},
},
account: {
fields: {
providerId: 'provider',
accountId: 'providerAccountId',
refreshToken: 'refresh_token',
accessToken: 'access_token',
idToken: 'id_token',
},
},
advanced: {
generateId: false,
},
socialProviders: {
google: {
clientId: '',
clientSecret: '',
},
},
emailAndPassword: {
enabled: true,
requireEmailVerification: false,
// maxPasswordLength: 128,
// minPasswordLength: 8,
password: {
hash: async (password) => hash(password, SALT_ROUNDS),
verify: async ({ hash, password }) => compare(password, hash),
},
},
});

View File

@@ -1,24 +0,0 @@
import type { BetterAuthClientPlugin } from 'better-auth';
import type { passkeyPlugin } from './index';
type PasskeyPlugin = typeof passkeyPlugin;
export const passkeyClientPlugin = () => {
const passkeySignin = () => {
//
// credential: JSON.stringify(credential),
// callbackUrl,
};
return {
id: 'passkeyPlugin',
getActions: () => ({
signIn: {
passkey: () => passkeySignin,
},
}),
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
$InferServerPlugin: {} as ReturnType<PasskeyPlugin>,
} satisfies BetterAuthClientPlugin;
};

View File

@@ -1,165 +0,0 @@
import type { BetterAuthPlugin } from 'better-auth';
import { createAuthEndpoint, createAuthMiddleware } from 'better-auth/plugins';
export const passkeyPlugin = () =>
({
id: 'passkeyPlugin',
schema: {
user: {
fields: {
// twoFactorEnabled: {
// type: 'boolean',
// required: false,
// },
// twoFactorBackupCodes: {
// type: 'string',
// required: false,
// },
// twoFactorSecret: {
// type: 'string',
// required: false,
// },
// birthday: {
// type: 'date', // string, number, boolean, date
// required: true, // if the field should be required on a new record. (default: false)
// unique: false, // if the field should be unique. (default: false)
// reference: null, // if the field is a reference to another table. (default: null)
// },
},
},
},
endpoints: {
authorize: createAuthEndpoint(
'/passkey/authorize',
{
method: 'POST',
// use: [],
},
async (ctx) => {
const csrfToken = credentials?.csrfToken;
if (typeof csrfToken !== 'string' || csrfToken.length === 0) {
throw new AppError(AppErrorCode.INVALID_REQUEST);
}
let requestBodyCrediential: TAuthenticationResponseJSONSchema | null = null;
try {
const parsedBodyCredential = JSON.parse(req.body?.credential);
requestBodyCrediential = ZAuthenticationResponseJSONSchema.parse(parsedBodyCredential);
} catch {
throw new AppError(AppErrorCode.INVALID_REQUEST);
}
const challengeToken = await prisma.anonymousVerificationToken
.delete({
where: {
id: csrfToken,
},
})
.catch(() => null);
if (!challengeToken) {
return null;
}
if (challengeToken.expiresAt < new Date()) {
throw new AppError(AppErrorCode.EXPIRED_CODE);
}
const passkey = await prisma.passkey.findFirst({
where: {
credentialId: Buffer.from(requestBodyCrediential.id, 'base64'),
},
include: {
User: {
select: {
id: true,
email: true,
name: true,
emailVerified: true,
},
},
},
});
if (!passkey) {
throw new AppError(AppErrorCode.NOT_SETUP);
}
const user = passkey.User;
const { rpId, origin } = getAuthenticatorOptions();
const verification = await verifyAuthenticationResponse({
response: requestBodyCrediential,
expectedChallenge: challengeToken.token,
expectedOrigin: origin,
expectedRPID: rpId,
authenticator: {
credentialID: new Uint8Array(Array.from(passkey.credentialId)),
credentialPublicKey: new Uint8Array(passkey.credentialPublicKey),
counter: Number(passkey.counter),
},
}).catch(() => null);
const requestMetadata = extractNextAuthRequestMetadata(req);
if (!verification?.verified) {
await prisma.userSecurityAuditLog.create({
data: {
userId: user.id,
ipAddress: requestMetadata.ipAddress,
userAgent: requestMetadata.userAgent,
type: UserSecurityAuditLogType.SIGN_IN_PASSKEY_FAIL,
},
});
return null;
}
await prisma.passkey.update({
where: {
id: passkey.id,
},
data: {
lastUsedAt: new Date(),
counter: verification.authenticationInfo.newCounter,
},
});
return {
id: Number(user.id),
email: user.email,
name: user.name,
emailVerified: user.emailVerified?.toISOString() ?? null,
} satisfies User;
},
),
},
hooks: {
before: [
{
matcher: (context) => context.path.startsWith('/sign-in/email'),
handler: createAuthMiddleware(async (ctx) => {
console.log('here...');
const { birthday } = ctx.body;
if ((!birthday) instanceof Date) {
throw new APIError('BAD_REQUEST', { message: 'Birthday must be of type Date.' });
}
const today = new Date();
const fiveYearsAgo = new Date(today.setFullYear(today.getFullYear() - 5));
if (birthday >= fiveYearsAgo) {
throw new APIError('BAD_REQUEST', { message: 'User must be above 5 years old.' });
}
return { context: ctx };
}),
},
],
},
}) satisfies BetterAuthPlugin;

View File

@@ -1,74 +0,0 @@
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
isRouteErrorResponse,
} from 'react-router';
import type { Route } from './+types/root';
import stylesheet from './app.css?url';
export const links: Route.LinksFunction = () => [
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
{
rel: 'preconnect',
href: 'https://fonts.gstatic.com',
crossOrigin: 'anonymous',
},
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
},
{ rel: 'stylesheet', href: stylesheet },
];
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = 'Oops!';
let details = 'An unexpected error occurred.';
let stack: string | undefined;
if (isRouteErrorResponse(error)) {
message = error.status === 404 ? '404' : 'Error';
details =
error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
} else if (import.meta.env.DEV && error && error instanceof Error) {
details = error.message;
stack = error.stack;
}
return (
<main className="container mx-auto p-4 pt-16">
<h1>{message}</h1>
<p>{details}</p>
{stack && (
<pre className="w-full overflow-x-auto p-4">
<code>{stack}</code>
</pre>
)}
</main>
);
}

View File

@@ -1,4 +0,0 @@
import { type RouteConfig } from '@react-router/dev/routes';
import { flatRoutes } from '@react-router/fs-routes';
export default flatRoutes() satisfies RouteConfig;

View File

@@ -1,193 +0,0 @@
import { useState } from 'react';
import { Button } from '@documenso/ui/primitives/button';
import { authClient, signOut, useSession } from '~/lib/auth-client';
import { auth } from '~/lib/auth.server';
import type { Route } from '../+types/root';
export function meta({}: Route.MetaArgs) {
return [
{ title: 'New React Router App' },
{ name: 'description', content: 'Welcome to React Router!' },
];
}
export async function loader({ params, request, context }: Route.LoaderArgs) {
const session = await auth.api.getSession({
query: {
disableCookieCache: true,
},
headers: request.headers, // pass the headers
});
return {
session,
};
}
export function clientLoader({ params }: Route.ClientLoaderArgs) {
return {
session: authClient.getSession(),
};
}
export default function Home({ loaderData }: Route.ComponentProps) {
const { data } = useSession();
const [email, setEmail] = useState('deepfriedcoconut@gmail.com');
const [password, setPassword] = useState('password');
const signIn = async () => {
await authClient.signIn.email(
{
email,
password,
},
{
onRequest: (ctx) => {
// show loading state
},
onSuccess: (ctx) => {
console.log('success');
// redirect to home
},
onError: (ctx) => {
console.log(ctx.error);
alert(ctx.error);
},
},
);
};
const signUp = async () => {
await authClient.signUp.email(
{
email,
password,
name: '',
},
{
onRequest: (ctx) => {
// show loading state
},
onSuccess: (ctx) => {
console.log(ctx);
// redirect to home
},
onError: (ctx) => {
console.log(ctx.error);
alert(ctx.error);
},
},
);
};
return (
<main className="flex flex-col items-center justify-center pb-4 pt-16">
<h1>Status: {data ? 'Authenticated' : 'Not Authenticated'}</h1>
{data ? (
<>
<div>
<p>Session data</p>
<p className="mt-2 max-w-2xl text-xs">{JSON.stringify(data, null, 2)}</p>
</div>
<div className="space-x-2">
<Button
onClick={() => {
authClient.twoFactor
.enable({
password: 'password', // user password required
})
.catch((e) => {
console.log(e);
});
}}
>
Enable 2FA
</Button>
<Button
variant="destructive"
onClick={() => {
authClient.twoFactor.disable({
password: 'password',
});
}}
>
Disable 2FA
</Button>
</div>
<button onClick={() => signOut()}>signout</button>
</>
) : (
<>
<div className="">
<h2>Sign In</h2>
<input
className="border border-blue-500"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
className="border border-blue-500"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit" onClick={signIn}>
Sign In
</button>
</div>
<div className="mt-8">
<h2>Sign Up</h2>
<input
type="email"
className="border border-blue-500"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
className="border border-blue-500"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit" onClick={signUp}>
Sign Up
</button>
</div>
<button
onClick={() => {
authClient.signIn.social({
provider: 'google',
});
}}
>
google
</button>
<button
onClick={async () => {
const response = await authClient.signIn.passkey();
console.log(response);
}}
>
passkey
</button>
</>
)}
</main>
);
}

View File

@@ -1,12 +0,0 @@
// Adjust the path as necessary
import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node';
import { auth } from '~/lib/auth.server';
export function loader({ request }: LoaderFunctionArgs) {
return auth.handler(request);
}
export function action({ request }: ActionFunctionArgs) {
return auth.handler(request);
}

View File

@@ -1,40 +0,0 @@
import { useState } from 'react';
import { authClient } from '~/lib/auth-client';
export default function SignIn() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const signIn = async () => {
await authClient.signIn.email(
{
email,
password,
},
{
onRequest: (ctx) => {
// show loading state
},
onSuccess: (ctx) => {
// redirect to home
},
onError: (ctx) => {
alert(ctx.error);
},
},
);
};
return (
<div>
<h2>Sign In</h2>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<button type="submit" onClick={signIn}>
Sign In
</button>
</div>
);
}

View File

@@ -1,43 +0,0 @@
{
"name": "@documenso/remix",
"type": "module",
"private": true,
"scripts": {
"build": "cross-env NODE_ENV=production react-router build",
"dev": "tsx watch --ignore \"vite.config.ts*\" server/main.ts",
"start": "cross-env NODE_ENV=production node dist/server/index.js",
"typecheck": "react-router typegen && tsc"
},
"dependencies": {
"@documenso/api": "*",
"@documenso/assets": "*",
"@documenso/ee": "*",
"@documenso/lib": "*",
"@documenso/prisma": "*",
"@documenso/trpc": "*",
"@documenso/ui": "*",
"@epic-web/remember": "^1.1.0",
"@hono/node-server": "^1.13.7",
"@react-router/fs-routes": "^7.1.1",
"@react-router/node": "^7.1.1",
"@react-router/serve": "^7.1.1",
"better-auth": "^1.1.9",
"hono": "^4.6.15",
"isbot": "^5.1.17",
"react": "^18",
"react-dom": "^18",
"react-router": "^7.1.1",
"remix-hono": "^0.0.18"
},
"devDependencies": {
"@react-router/dev": "^7.1.1",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"cross-env": "^7.0.3",
"tsx": "^4.11.0",
"typescript": "5.7.2",
"vite": "^5.4.11",
"vite-tsconfig-paths": "^5.1.4"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,7 +0,0 @@
import type { Config } from '@react-router/dev/config';
export default {
appDirectory: 'app',
// Server-side render by default, to enable SPA mode set this to `false`
ssr: true,
} satisfies Config;

View File

@@ -1,45 +0,0 @@
import { remember } from '@epic-web/remember';
import { type HttpBindings } from '@hono/node-server';
import { Hono } from 'hono';
import { reactRouter } from 'remix-hono/handler';
type Bindings = HttpBindings;
const app = new Hono<{ Bindings: Bindings }>();
const isProduction = process.env.NODE_ENV === 'production';
const viteDevServer = isProduction
? undefined
: await import('vite').then(async (vite) =>
vite.createServer({
server: { middlewareMode: true },
}),
);
const reactRouterMiddleware = remember('reactRouterMiddleware', async () =>
reactRouter({
mode: isProduction ? 'production' : 'development',
build: isProduction
? // @ts-expect-error build/server/index.js is a build artifact
await import('../build/server/index.js')
: async () => viteDevServer!.ssrLoadModule('virtual:react-router/server-build'),
}),
);
// app.get('/', (c) => c.text('Hello, world!'));
if (viteDevServer) {
app.use('*', async (c, next) => {
return new Promise((resolve) => {
viteDevServer.middlewares(c.env.incoming, c.env.outgoing, () => resolve(next()));
});
});
}
app.use('*', async (c, next) => {
const middleware = await reactRouterMiddleware;
return middleware(c, next);
});
export default app;

View File

@@ -1,7 +0,0 @@
import { serve } from '@hono/node-server';
import app from './app';
serve(app, (info) => {
console.log(`Server is running on http://localhost:${info.port}`);
});

View File

@@ -1,18 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const baseConfig = require('@documenso/tailwind-config');
const path = require('path');
module.exports = {
...baseConfig,
content: [
...baseConfig.content,
'./app/**/*.{ts,tsx}',
`${path.join(require.resolve('@documenso/ui'), '..')}/components/**/*.{ts,tsx}`,
`${path.join(require.resolve('@documenso/ui'), '..')}/icons/**/*.{ts,tsx}`,
`${path.join(require.resolve('@documenso/ui'), '..')}/lib/**/*.{ts,tsx}`,
`${path.join(require.resolve('@documenso/ui'), '..')}/primitives/**/*.{ts,tsx}`,
`${path.join(require.resolve('@documenso/email'), '..')}/templates/**/*.{ts,tsx}`,
`${path.join(require.resolve('@documenso/email'), '..')}/template-components/**/*.{ts,tsx}`,
`${path.join(require.resolve('@documenso/email'), '..')}/providers/**/*.{ts,tsx}`,
],
};

View File

@@ -1,27 +0,0 @@
{
"include": [
"**/*",
"**/.server/**/*",
"**/.client/**/*",
".react-router/types/**/*"
],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": ["node", "vite/client"],
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"rootDirs": [".", "./.react-router/types"],
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"]
},
"esModuleInterop": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true
}
}

View File

@@ -1,17 +0,0 @@
import { reactRouter } from '@react-router/dev/vite';
import autoprefixer from 'autoprefixer';
import tailwindcss from 'tailwindcss';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
css: {
postcss: {
plugins: [tailwindcss, autoprefixer],
},
},
plugins: [reactRouter(), tsconfigPaths()],
optimizeDeps: {
exclude: ['@node-rs/bcrypt'],
},
});

View File

@@ -1,6 +1,6 @@
{ {
"name": "@documenso/web", "name": "@documenso/web",
"version": "1.9.0-rc.5", "version": "1.9.0-rc.11",
"private": true, "private": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
@@ -26,7 +26,6 @@
"@lingui/react": "^4.11.3", "@lingui/react": "^4.11.3",
"@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/browser": "^9.0.1",
"@simplewebauthn/server": "^9.0.3", "@simplewebauthn/server": "^9.0.3",
"@tanstack/react-query": "^4.29.5",
"colord": "^2.9.3", "colord": "^2.9.3",
"cookie-es": "^1.0.0", "cookie-es": "^1.0.0",
"formidable": "^2.1.1", "formidable": "^2.1.1",
@@ -55,7 +54,7 @@
"recharts": "^2.7.2", "recharts": "^2.7.2",
"remeda": "^2.17.3", "remeda": "^2.17.3",
"sharp": "0.32.6", "sharp": "0.32.6",
"trpc-openapi": "^1.2.0", "trpc-to-openapi": "2.0.4",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",
"ua-parser-js": "^1.0.37", "ua-parser-js": "^1.0.37",
"uqr": "^0.1.2", "uqr": "^0.1.2",
@@ -68,11 +67,11 @@
"@simplewebauthn/types": "^9.0.1", "@simplewebauthn/types": "^9.0.1",
"@types/formidable": "^2.0.6", "@types/formidable": "^2.0.6",
"@types/luxon": "^3.3.1", "@types/luxon": "^3.3.1",
"@types/node": "20.1.0", "@types/node": "^20",
"@types/papaparse": "^5.3.14", "@types/papaparse": "^5.3.14",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
"typescript": "5.7.2" "typescript": "5.6.2"
} }
} }

View File

@@ -28,7 +28,7 @@ export const AdminActions = ({ className, document, recipients }: AdminActionsPr
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { mutate: resealDocument, isLoading: isResealDocumentLoading } = const { mutate: resealDocument, isPending: isResealDocumentLoading } =
trpc.admin.resealDocument.useMutation({ trpc.admin.resealDocument.useMutation({
onSuccess: () => { onSuccess: () => {
toast({ toast({

View File

@@ -59,7 +59,7 @@ export default async function AdminDocumentDetailsPage({ params }: AdminDocument
<Trans>Admin Actions</Trans> <Trans>Admin Actions</Trans>
</h2> </h2>
<AdminActions className="mt-2" document={document} recipients={document.Recipient} /> <AdminActions className="mt-2" document={document} recipients={document.recipients} />
<hr className="my-4" /> <hr className="my-4" />
<h2 className="text-lg font-semibold"> <h2 className="text-lg font-semibold">
@@ -68,7 +68,7 @@ export default async function AdminDocumentDetailsPage({ params }: AdminDocument
<div className="mt-4"> <div className="mt-4">
<Accordion type="multiple" className="space-y-4"> <Accordion type="multiple" className="space-y-4">
{document.Recipient.map((recipient) => ( {document.recipients.map((recipient) => (
<AccordionItem <AccordionItem
key={recipient.id} key={recipient.id}
value={recipient.id.toString()} value={recipient.id.toString()}

View File

@@ -39,9 +39,9 @@ type TAdminUpdateRecipientFormSchema = z.infer<typeof ZAdminUpdateRecipientFormS
export type RecipientItemProps = { export type RecipientItemProps = {
recipient: Recipient & { recipient: Recipient & {
Field: Array< fields: Array<
Field & { Field & {
Signature: Signature | null; signature: Signature | null;
} }
>; >;
}; };
@@ -89,13 +89,13 @@ export const RecipientItem = ({ recipient }: RecipientItemProps) => {
accessorKey: 'signature', accessorKey: 'signature',
cell: ({ row }) => ( cell: ({ row }) => (
<div> <div>
{row.original.Signature?.typedSignature && ( {row.original.signature?.typedSignature && (
<span>{row.original.Signature.typedSignature}</span> <span>{row.original.signature.typedSignature}</span>
)} )}
{row.original.Signature?.signatureImageAsBase64 && ( {row.original.signature?.signatureImageAsBase64 && (
<img <img
src={row.original.Signature.signatureImageAsBase64} src={row.original.signature.signatureImageAsBase64}
alt="Signature" alt="Signature"
className="h-12 w-full dark:invert" className="h-12 w-full dark:invert"
/> />
@@ -103,7 +103,7 @@ export const RecipientItem = ({ recipient }: RecipientItemProps) => {
</div> </div>
), ),
}, },
] satisfies DataTableColumnDef<(typeof recipient)['Field'][number]>[]; ] satisfies DataTableColumnDef<(typeof recipient)['fields'][number]>[];
}, []); }, []);
const onUpdateRecipientFormSubmit = async ({ name, email }: TAdminUpdateRecipientFormSchema) => { const onUpdateRecipientFormSubmit = async ({ name, email }: TAdminUpdateRecipientFormSchema) => {
@@ -190,7 +190,7 @@ export const RecipientItem = ({ recipient }: RecipientItemProps) => {
<Trans>Fields</Trans> <Trans>Fields</Trans>
</h2> </h2>
<DataTable columns={columns} data={recipient.Field} /> <DataTable columns={columns} data={recipient.fields} />
</div> </div>
); );
}; };

View File

@@ -8,7 +8,6 @@ import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import type { Document } from '@documenso/prisma/client'; import type { Document } from '@documenso/prisma/client';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@@ -36,7 +35,7 @@ export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialo
const [reason, setReason] = useState(''); const [reason, setReason] = useState('');
const { mutateAsync: deleteDocument, isLoading: isDeletingDocument } = const { mutateAsync: deleteDocument, isPending: isDeletingDocument } =
trpc.admin.deleteDocument.useMutation(); trpc.admin.deleteDocument.useMutation();
const handleDeleteDocument = async () => { const handleDeleteDocument = async () => {
@@ -55,29 +54,20 @@ export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialo
router.push('/admin/documents'); router.push('/admin/documents');
} catch (err) { } catch (err) {
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
toast({
title: _(msg`An error occurred`),
description: err.message,
variant: 'destructive',
});
} else {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An unknown error occurred`),
variant: 'destructive', variant: 'destructive',
description: description:
err.message ??
'We encountered an unknown error while attempting to delete your document. Please try again later.', 'We encountered an unknown error while attempting to delete your document. Please try again later.',
}); });
} }
}
}; };
return ( return (
<div> <div>
<div> <div>
<Alert <Alert
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row " className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral" variant="neutral"
> >
<div> <div>

View File

@@ -37,7 +37,7 @@ export const AdminDocumentResults = () => {
const page = searchParams?.get?.('page') ? Number(searchParams.get('page')) : undefined; const page = searchParams?.get?.('page') ? Number(searchParams.get('page')) : undefined;
const perPage = searchParams?.get?.('perPage') ? Number(searchParams.get('perPage')) : undefined; const perPage = searchParams?.get?.('perPage') ? Number(searchParams.get('perPage')) : undefined;
const { data: findDocumentsData, isLoading: isFindDocumentsLoading } = const { data: findDocumentsData, isPending: isFindDocumentsLoading } =
trpc.admin.findDocuments.useQuery( trpc.admin.findDocuments.useQuery(
{ {
query: debouncedTerm, query: debouncedTerm,
@@ -45,7 +45,7 @@ export const AdminDocumentResults = () => {
perPage: perPage || 20, perPage: perPage || 20,
}, },
{ {
keepPreviousData: true, placeholderData: (previousData) => previousData,
}, },
); );
@@ -86,14 +86,14 @@ export const AdminDocumentResults = () => {
header: _(msg`Owner`), header: _(msg`Owner`),
accessorKey: 'owner', accessorKey: 'owner',
cell: ({ row }) => { cell: ({ row }) => {
const avatarFallbackText = row.original.User.name const avatarFallbackText = row.original.user.name
? extractInitials(row.original.User.name) ? extractInitials(row.original.user.name)
: row.original.User.email.slice(0, 1).toUpperCase(); : row.original.user.email.slice(0, 1).toUpperCase();
return ( return (
<Tooltip delayDuration={200}> <Tooltip delayDuration={200}>
<TooltipTrigger> <TooltipTrigger>
<Link href={`/admin/users/${row.original.User.id}`}> <Link href={`/admin/users/${row.original.user.id}`}>
<Avatar className="dark:border-border h-12 w-12 border-2 border-solid border-white"> <Avatar className="dark:border-border h-12 w-12 border-2 border-solid border-white">
<AvatarFallback className="text-xs text-gray-400"> <AvatarFallback className="text-xs text-gray-400">
{avatarFallbackText} {avatarFallbackText}
@@ -110,8 +110,8 @@ export const AdminDocumentResults = () => {
</Avatar> </Avatar>
<div className="text-muted-foreground flex flex-col text-sm"> <div className="text-muted-foreground flex flex-col text-sm">
<span>{row.original.User.name}</span> <span>{row.original.user.name}</span>
<span>{row.original.User.email}</span> <span>{row.original.user.email}</span>
</div> </div>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>

View File

@@ -13,7 +13,6 @@ import {
SITE_SETTINGS_BANNER_ID, SITE_SETTINGS_BANNER_ID,
ZSiteSettingsBannerSchema, ZSiteSettingsBannerSchema,
} from '@documenso/lib/server-only/site-settings/schemas/banner'; } from '@documenso/lib/server-only/site-settings/schemas/banner';
import { TRPCClientError } from '@documenso/trpc/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 { ColorPicker } from '@documenso/ui/primitives/color-picker'; import { ColorPicker } from '@documenso/ui/primitives/color-picker';
@@ -59,7 +58,7 @@ export function BannerForm({ banner }: BannerFormProps) {
const enabled = form.watch('enabled'); const enabled = form.watch('enabled');
const { mutateAsync: updateSiteSetting, isLoading: isUpdateSiteSettingLoading } = const { mutateAsync: updateSiteSetting, isPending: isUpdateSiteSettingLoading } =
trpcReact.admin.updateSiteSetting.useMutation(); trpcReact.admin.updateSiteSetting.useMutation();
const onBannerUpdate = async ({ id, enabled, data }: TBannerFormSchema) => { const onBannerUpdate = async ({ id, enabled, data }: TBannerFormSchema) => {
@@ -78,13 +77,6 @@ export function BannerForm({ banner }: BannerFormProps) {
router.refresh(); router.refresh();
} catch (err) { } catch (err) {
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
toast({
title: _(msg`An error occurred`),
description: err.message,
variant: 'destructive',
});
} else {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An unknown error occurred`),
variant: 'destructive', variant: 'destructive',
@@ -93,7 +85,6 @@ export function BannerForm({ banner }: BannerFormProps) {
), ),
}); });
} }
}
}; };
return ( return (

View File

@@ -6,9 +6,10 @@ import { useRouter } from 'next/navigation';
import { Trans, msg } from '@lingui/macro'; import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { match } from 'ts-pattern';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { User } from '@documenso/prisma/client'; import type { User } from '@documenso/prisma/client';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@@ -37,7 +38,7 @@ export const DeleteUserDialog = ({ className, user }: DeleteUserDialogProps) =>
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const { mutateAsync: deleteUser, isLoading: isDeletingUser } = const { mutateAsync: deleteUser, isPending: isDeletingUser } =
trpc.admin.deleteUser.useMutation(); trpc.admin.deleteUser.useMutation();
const onDeleteAccount = async () => { const onDeleteAccount = async () => {
@@ -54,23 +55,19 @@ export const DeleteUserDialog = ({ className, user }: DeleteUserDialogProps) =>
router.push('/admin/users'); router.push('/admin/users');
} catch (err) { } catch (err) {
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') { const error = AppError.parseError(err);
const errorMessage = match(error.code)
.with(AppErrorCode.NOT_FOUND, () => msg`User not found.`)
.with(AppErrorCode.UNAUTHORIZED, () => msg`You are not authorized to delete this user.`)
.otherwise(() => msg`An error occurred while deleting the user.`);
toast({ toast({
title: _(msg`An error occurred`), title: _(msg`Error`),
description: err.message, description: _(errorMessage),
variant: 'destructive', variant: 'destructive',
duration: 7500,
}); });
} else {
toast({
title: _(msg`An unknown error occurred`),
variant: 'destructive',
description:
err.message ??
_(
msg`We encountered an unknown error while attempting to delete your account. Please try again later.`,
),
});
}
} }
}; };

View File

@@ -34,7 +34,7 @@ export const DisableUserDialog = ({ className, userToDisable }: DisableUserDialo
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const { mutateAsync: disableUser, isLoading: isDisablingUser } = const { mutateAsync: disableUser, isPending: isDisablingUser } =
trpc.admin.disableUser.useMutation(); trpc.admin.disableUser.useMutation();
const onDisableAccount = async () => { const onDisableAccount = async () => {

View File

@@ -34,7 +34,7 @@ export const EnableUserDialog = ({ className, userToEnable }: EnableUserDialogPr
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const { mutateAsync: enableUser, isLoading: isEnablingUser } = const { mutateAsync: enableUser, isPending: isEnablingUser } =
trpc.admin.enableUser.useMutation(); trpc.admin.enableUser.useMutation();
const onEnableAccount = async () => { const onEnableAccount = async () => {

View File

@@ -22,8 +22,8 @@ type UserData = {
name: string | null; name: string | null;
email: string; email: string;
roles: Role[]; roles: Role[];
Subscription?: SubscriptionLite[] | null; subscriptions?: SubscriptionLite[] | null;
Document: DocumentLite[]; documents: DocumentLite[];
}; };
type SubscriptionLite = Pick< type SubscriptionLite = Pick<
@@ -81,7 +81,7 @@ export const UsersDataTable = ({
header: _(msg`Subscription`), header: _(msg`Subscription`),
accessorKey: 'subscription', accessorKey: 'subscription',
cell: ({ row }) => { cell: ({ row }) => {
const foundIndividualSubscription = (row.original.Subscription ?? []).find((sub) => const foundIndividualSubscription = (row.original.subscriptions ?? []).find((sub) =>
individualPriceIds.includes(sub.priceId), individualPriceIds.includes(sub.priceId),
); );
@@ -92,7 +92,7 @@ export const UsersDataTable = ({
header: _(msg`Documents`), header: _(msg`Documents`),
accessorKey: 'documents', accessorKey: 'documents',
cell: ({ row }) => { cell: ({ row }) => {
return <div>{row.original.Document.length}</div>; return <div>{row.original.documents?.length}</div>;
}, },
}, },
{ {

View File

@@ -18,14 +18,14 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
export type DocumentPageViewButtonProps = { export type DocumentPageViewButtonProps = {
document: Document & { document: Document & {
User: Pick<User, 'id' | 'name' | 'email'>; user: Pick<User, 'id' | 'name' | 'email'>;
Recipient: Recipient[]; recipients: Recipient[];
team: Pick<Team, 'id' | 'url'> | null; team: Pick<Team, 'id' | 'url'> | null;
}; };
team?: Pick<Team, 'id' | 'url'>; team?: Pick<Team, 'id' | 'url'>;
}; };
export const DocumentPageViewButton = ({ document, team }: DocumentPageViewButtonProps) => { export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps) => {
const { data: session } = useSession(); const { data: session } = useSession();
const { toast } = useToast(); const { toast } = useToast();
const { _ } = useLingui(); const { _ } = useLingui();
@@ -34,7 +34,7 @@ export const DocumentPageViewButton = ({ document, team }: DocumentPageViewButto
return null; return null;
} }
const recipient = document.Recipient.find((recipient) => recipient.email === session.user.email); const recipient = document.recipients.find((recipient) => recipient.email === session.user.email);
const isRecipient = !!recipient; const isRecipient = !!recipient;
const isPending = document.status === DocumentStatus.PENDING; const isPending = document.status === DocumentStatus.PENDING;
@@ -46,10 +46,16 @@ export const DocumentPageViewButton = ({ document, team }: DocumentPageViewButto
const onDownloadClick = async () => { const onDownloadClick = async () => {
try { try {
const documentWithData = await trpcClient.document.getDocumentById.query({ const documentWithData = await trpcClient.document.getDocumentById.query(
{
documentId: document.id, documentId: document.id,
teamId: team?.id, },
}); {
context: {
teamId: document.team?.id?.toString(),
},
},
);
const documentData = documentWithData?.documentData; const documentData = documentWithData?.documentData;

View File

@@ -41,8 +41,8 @@ import { DuplicateDocumentDialog } from '../duplicate-document-dialog';
export type DocumentPageViewDropdownProps = { export type DocumentPageViewDropdownProps = {
document: Document & { document: Document & {
User: Pick<User, 'id' | 'name' | 'email'>; user: Pick<User, 'id' | 'name' | 'email'>;
Recipient: Recipient[]; recipients: Recipient[];
team: Pick<Team, 'id' | 'url'> | null; team: Pick<Team, 'id' | 'url'> | null;
}; };
team?: Pick<Team, 'id' | 'url'> & { teamEmail: TeamEmail | null }; team?: Pick<Team, 'id' | 'url'> & { teamEmail: TeamEmail | null };
@@ -60,9 +60,9 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
return null; return null;
} }
const recipient = document.Recipient.find((recipient) => recipient.email === session.user.email); const recipient = document.recipients.find((recipient) => recipient.email === session.user.email);
const isOwner = document.User.id === session.user.id; const isOwner = document.user.id === session.user.id;
const isDraft = document.status === DocumentStatus.DRAFT; const isDraft = document.status === DocumentStatus.DRAFT;
const isPending = document.status === DocumentStatus.PENDING; const isPending = document.status === DocumentStatus.PENDING;
const isDeleted = document.deletedAt !== null; const isDeleted = document.deletedAt !== null;
@@ -74,10 +74,16 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
const onDownloadClick = async () => { const onDownloadClick = async () => {
try { try {
const documentWithData = await trpcClient.document.getDocumentById.query({ const documentWithData = await trpcClient.document.getDocumentById.query(
{
documentId: document.id, documentId: document.id,
teamId: team?.id, },
}); {
context: {
teamId: team?.id?.toString(),
},
},
);
const documentData = documentWithData?.documentData; const documentData = documentWithData?.documentData;
@@ -95,7 +101,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
} }
}; };
const nonSignedRecipients = document.Recipient.filter((item) => item.signingStatus !== 'SIGNED'); const nonSignedRecipients = document.recipients.filter((item) => item.signingStatus !== 'SIGNED');
return ( return (
<DropdownMenu> <DropdownMenu>
@@ -150,7 +156,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
{canManageDocument && ( {canManageDocument && (
<DocumentRecipientLinkCopyDialog <DocumentRecipientLinkCopyDialog
recipients={document.Recipient} recipients={document.recipients}
trigger={ trigger={
<DropdownMenuItem <DropdownMenuItem
disabled={!isPending || isDeleted} disabled={!isPending || isDeleted}

View File

@@ -12,8 +12,8 @@ import type { Document, Recipient, User } from '@documenso/prisma/client';
export type DocumentPageViewInformationProps = { export type DocumentPageViewInformationProps = {
userId: number; userId: number;
document: Document & { document: Document & {
User: Pick<User, 'id' | 'name' | 'email'>; user: Pick<User, 'id' | 'name' | 'email'>;
Recipient: Recipient[]; recipients: Recipient[];
}; };
}; };
@@ -29,7 +29,8 @@ export const DocumentPageViewInformation = ({
return [ return [
{ {
description: msg`Uploaded by`, description: msg`Uploaded by`,
value: userId === document.userId ? _(msg`You`) : document.User.name ?? document.User.email, value:
userId === document.userId ? _(msg`You`) : (document.user.name ?? document.user.email),
}, },
{ {
description: msg`Created`, description: msg`Created`,

View File

@@ -28,7 +28,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
export type DocumentPageViewRecipientsProps = { export type DocumentPageViewRecipientsProps = {
document: Document & { document: Document & {
Recipient: Recipient[]; recipients: Recipient[];
}; };
documentRootPath: string; documentRootPath: string;
}; };
@@ -40,7 +40,7 @@ export const DocumentPageViewRecipients = ({
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const recipients = document.Recipient; const recipients = document.recipients;
return ( return (
<section className="dark:bg-background border-border bg-widget flex flex-col rounded-xl border"> <section className="dark:bg-background border-border bg-widget flex flex-col rounded-xl border">

View File

@@ -71,7 +71,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
const documentVisibility = document?.visibility; const documentVisibility = document?.visibility;
const currentTeamMemberRole = team?.currentTeamMember?.role; const currentTeamMemberRole = team?.currentTeamMember?.role;
const isRecipient = document?.Recipient.find((recipient) => recipient.email === user.email); const isRecipient = document?.recipients.find((recipient) => recipient.email === user.email);
let canAccessDocument = true; let canAccessDocument = true;
if (team && !isRecipient && document?.userId !== user.id) { if (team && !isRecipient && document?.userId !== user.id) {
@@ -125,12 +125,13 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
getFieldsForDocument({ getFieldsForDocument({
documentId, documentId,
userId: user.id, userId: user.id,
teamId: team?.id,
}), }),
]); ]);
const documentWithRecipients = { const documentWithRecipients = {
...document, ...document,
Recipient: recipients, recipients,
}; };
return ( return (

View File

@@ -12,7 +12,7 @@ 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 type { TGetDocumentWithDetailsByIdResponse } from '@documenso/lib/server-only/document/get-document-with-details-by-id'; import type { TDocument } from '@documenso/lib/types/document';
import { DocumentDistributionMethod, DocumentStatus } from '@documenso/prisma/client'; import { DocumentDistributionMethod, DocumentStatus } 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';
@@ -35,7 +35,7 @@ import { useOptionalCurrentTeam } from '~/providers/team';
export type EditDocumentFormProps = { export type EditDocumentFormProps = {
className?: string; className?: string;
initialDocument: TGetDocumentWithDetailsByIdResponse; initialDocument: TDocument;
documentRootPath: string; documentRootPath: string;
isDocumentEnterprise: boolean; isDocumentEnterprise: boolean;
}; };
@@ -64,7 +64,6 @@ export const EditDocumentForm = ({
trpc.document.getDocumentWithDetailsById.useQuery( trpc.document.getDocumentWithDetailsById.useQuery(
{ {
documentId: initialDocument.id, documentId: initialDocument.id,
teamId: team?.id,
}, },
{ {
initialData: initialDocument, initialData: initialDocument,
@@ -72,15 +71,14 @@ export const EditDocumentForm = ({
}, },
); );
const { Recipient: recipients, Field: fields } = document; const { recipients, fields } = document;
const { mutateAsync: setSettingsForDocument } = trpc.document.setSettingsForDocument.useMutation({ const { mutateAsync: updateDocument } = trpc.document.setSettingsForDocument.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => { onSuccess: (newData) => {
utils.document.getDocumentWithDetailsById.setData( utils.document.getDocumentWithDetailsById.setData(
{ {
documentId: initialDocument.id, documentId: initialDocument.id,
teamId: team?.id,
}, },
(oldData) => ({ ...(oldData || initialDocument), ...newData }), (oldData) => ({ ...(oldData || initialDocument), ...newData }),
); );
@@ -94,7 +92,6 @@ export const EditDocumentForm = ({
utils.document.getDocumentWithDetailsById.setData( utils.document.getDocumentWithDetailsById.setData(
{ {
documentId: initialDocument.id, documentId: initialDocument.id,
teamId: team?.id,
}, },
(oldData) => ({ ...(oldData || initialDocument), ...newData, id: Number(newData.id) }), (oldData) => ({ ...(oldData || initialDocument), ...newData, id: Number(newData.id) }),
); );
@@ -107,40 +104,20 @@ export const EditDocumentForm = ({
utils.document.getDocumentWithDetailsById.setData( utils.document.getDocumentWithDetailsById.setData(
{ {
documentId: initialDocument.id, documentId: initialDocument.id,
teamId: team?.id,
}, },
(oldData) => ({ ...(oldData || initialDocument), Field: newFields }), (oldData) => ({ ...(oldData || initialDocument), fields: newFields }),
); );
}, },
}); });
const { mutateAsync: updateTypedSignature } = const { mutateAsync: setRecipients } = trpc.recipient.setDocumentRecipients.useMutation({
trpc.document.updateTypedSignatureSettings.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.document.getDocumentWithDetailsById.setData(
{
documentId: initialDocument.id,
teamId: team?.id,
},
(oldData) => ({
...(oldData || initialDocument),
...newData,
id: Number(newData.id),
}),
);
},
});
const { mutateAsync: addSigners } = trpc.recipient.addSigners.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: ({ recipients: newRecipients }) => { onSuccess: ({ recipients: newRecipients }) => {
utils.document.getDocumentWithDetailsById.setData( utils.document.getDocumentWithDetailsById.setData(
{ {
documentId: initialDocument.id, documentId: initialDocument.id,
teamId: team?.id,
}, },
(oldData) => ({ ...(oldData || initialDocument), Recipient: newRecipients }), (oldData) => ({ ...(oldData || initialDocument), recipients: newRecipients }),
); );
}, },
}); });
@@ -151,7 +128,6 @@ export const EditDocumentForm = ({
utils.document.getDocumentWithDetailsById.setData( utils.document.getDocumentWithDetailsById.setData(
{ {
documentId: initialDocument.id, documentId: initialDocument.id,
teamId: team?.id,
}, },
(oldData) => ({ ...(oldData || initialDocument), ...newData }), (oldData) => ({ ...(oldData || initialDocument), ...newData }),
); );
@@ -205,9 +181,8 @@ export const EditDocumentForm = ({
try { try {
const { timezone, dateFormat, redirectUrl, language } = data.meta; const { timezone, dateFormat, redirectUrl, language } = data.meta;
await setSettingsForDocument({ await updateDocument({
documentId: document.id, documentId: document.id,
teamId: team?.id,
data: { data: {
title: data.title, title: data.title,
externalId: data.externalId || null, externalId: data.externalId || null,
@@ -246,10 +221,9 @@ export const EditDocumentForm = ({
signingOrder: data.signingOrder, signingOrder: data.signingOrder,
}), }),
addSigners({ setRecipients({
documentId: document.id, documentId: document.id,
teamId: team?.id, recipients: data.signers.map((signer) => ({
signers: data.signers.map((signer) => ({
...signer, ...signer,
// Explicitly set to null to indicate we want to remove auth if required. // Explicitly set to null to indicate we want to remove auth if required.
actionAuth: signer.actionAuth || null, actionAuth: signer.actionAuth || null,
@@ -279,9 +253,12 @@ export const EditDocumentForm = ({
fields: data.fields, fields: data.fields,
}); });
await updateTypedSignature({ await updateDocument({
documentId: document.id, documentId: document.id,
meta: {
typedSignatureEnabled: data.typedSignatureEnabled, typedSignatureEnabled: data.typedSignatureEnabled,
},
}); });
// Clear all field data from localStorage // Clear all field data from localStorage
@@ -313,7 +290,6 @@ export const EditDocumentForm = ({
try { try {
await sendDocument({ await sendDocument({
documentId: document.id, documentId: document.id,
teamId: team?.id,
meta: { meta: {
subject, subject,
message, message,

View File

@@ -52,7 +52,7 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
const documentVisibility = document?.visibility; const documentVisibility = document?.visibility;
const currentTeamMemberRole = team?.currentTeamMember?.role; const currentTeamMemberRole = team?.currentTeamMember?.role;
const isRecipient = document?.Recipient.find((recipient) => recipient.email === user.email); const isRecipient = document?.recipients.find((recipient) => recipient.email === user.email);
let canAccessDocument = true; let canAccessDocument = true;
if (!isRecipient && document?.userId !== user.id) { if (!isRecipient && document?.userId !== user.id) {
@@ -78,7 +78,7 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
redirect(`${documentRootPath}/${documentId}`); redirect(`${documentRootPath}/${documentId}`);
} }
const { documentMeta, Recipient: recipients } = document; const { documentMeta, recipients } = document;
if (documentMeta?.password) { if (documentMeta?.password) {
const key = DOCUMENSO_ENCRYPTION_KEY; const key = DOCUMENSO_ENCRYPTION_KEY;

View File

@@ -37,15 +37,14 @@ export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? [])); const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
const { data, isLoading, isInitialLoading, isLoadingError } = const { data, isLoading, isLoadingError } = trpc.document.findDocumentAuditLogs.useQuery(
trpc.document.findDocumentAuditLogs.useQuery(
{ {
documentId, documentId,
page: parsedSearchParams.page, page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage, perPage: parsedSearchParams.perPage,
}, },
{ {
keepPreviousData: true, placeholderData: (previousData) => previousData,
}, },
); );
@@ -132,7 +131,7 @@ export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps
enable: isLoadingError, enable: isLoadingError,
}} }}
skeleton={{ skeleton={{
enable: isLoading && isInitialLoading, enable: isLoading,
rows: 3, rows: 3,
component: ( component: (
<> <>

View File

@@ -77,9 +77,9 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
}, },
{ {
description: msg`Created by`, description: msg`Created by`,
value: document.User.name value: document.user.name
? `${document.User.name} (${document.User.email})` ? `${document.user.name} (${document.user.email})`
: document.User.email, : document.user.email,
}, },
{ {
description: msg`Date created`, description: msg`Date created`,

View File

@@ -15,20 +15,16 @@ export type DownloadAuditLogButtonProps = {
documentId: number; documentId: number;
}; };
export const DownloadAuditLogButton = ({ export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditLogButtonProps) => {
className,
teamId,
documentId,
}: DownloadAuditLogButtonProps) => {
const { toast } = useToast(); const { toast } = useToast();
const { _ } = useLingui(); const { _ } = useLingui();
const { mutateAsync: downloadAuditLogs, isLoading } = const { mutateAsync: downloadAuditLogs, isPending } =
trpc.document.downloadAuditLogs.useMutation(); trpc.document.downloadAuditLogs.useMutation();
const onDownloadAuditLogsClick = async () => { const onDownloadAuditLogsClick = async () => {
try { try {
const { url } = await downloadAuditLogs({ teamId, documentId }); const { url } = await downloadAuditLogs({ documentId });
const iframe = Object.assign(document.createElement('iframe'), { const iframe = Object.assign(document.createElement('iframe'), {
src: url, src: url,
@@ -74,10 +70,10 @@ export const DownloadAuditLogButton = ({
return ( return (
<Button <Button
className={cn('w-full sm:w-auto', className)} className={cn('w-full sm:w-auto', className)}
loading={isLoading} loading={isPending}
onClick={() => void onDownloadAuditLogsClick()} onClick={() => void onDownloadAuditLogsClick()}
> >
{!isLoading && <DownloadIcon className="mr-1.5 h-4 w-4" />} {!isPending && <DownloadIcon className="mr-1.5 h-4 w-4" />}
<Trans>Download Audit Logs</Trans> <Trans>Download Audit Logs</Trans>
</Button> </Button>
); );

View File

@@ -26,12 +26,12 @@ export const DownloadCertificateButton = ({
const { toast } = useToast(); const { toast } = useToast();
const { _ } = useLingui(); const { _ } = useLingui();
const { mutateAsync: downloadCertificate, isLoading } = const { mutateAsync: downloadCertificate, isPending } =
trpc.document.downloadCertificate.useMutation(); trpc.document.downloadCertificate.useMutation();
const onDownloadCertificatesClick = async () => { const onDownloadCertificatesClick = async () => {
try { try {
const { url } = await downloadCertificate({ documentId, teamId }); const { url } = await downloadCertificate({ documentId });
const iframe = Object.assign(document.createElement('iframe'), { const iframe = Object.assign(document.createElement('iframe'), {
src: url, src: url,
@@ -77,12 +77,12 @@ export const DownloadCertificateButton = ({
return ( return (
<Button <Button
className={cn('w-full sm:w-auto', className)} className={cn('w-full sm:w-auto', className)}
loading={isLoading} loading={isPending}
variant="outline" variant="outline"
disabled={documentStatus !== DocumentStatus.COMPLETED} disabled={documentStatus !== DocumentStatus.COMPLETED}
onClick={() => void onDownloadCertificatesClick()} onClick={() => void onDownloadCertificatesClick()}
> >
{!isLoading && <DownloadIcon className="mr-1.5 h-4 w-4" />} {!isPending && <DownloadIcon className="mr-1.5 h-4 w-4" />}
<Trans>Download Certificate</Trans> <Trans>Download Certificate</Trans>
</Button> </Button>
); );

View File

@@ -91,7 +91,7 @@ export const ResendDocumentActionItem = ({
const onFormSubmit = async ({ recipients }: TResendDocumentFormSchema) => { const onFormSubmit = async ({ recipients }: TResendDocumentFormSchema) => {
try { try {
await resendDocument({ documentId: document.id, recipients, teamId: team?.id }); await resendDocument({ documentId: document.id, recipients });
toast({ toast({
title: _(msg`Document re-sent`), title: _(msg`Document re-sent`),

View File

@@ -12,15 +12,14 @@ import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import type { Document, Recipient, Team, User } from '@documenso/prisma/client'; import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client'; import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import { trpc as trpcClient } from '@documenso/trpc/client'; import { trpc as trpcClient } from '@documenso/trpc/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 DataTableActionButtonProps = { export type DataTableActionButtonProps = {
row: Document & { row: Document & {
User: Pick<User, 'id' | 'name' | 'email'>; user: Pick<User, 'id' | 'name' | 'email'>;
Recipient: Recipient[]; recipients: Recipient[];
team: Pick<Team, 'id' | 'url'> | null; team: Pick<Team, 'id' | 'url'> | null;
}; };
team?: Pick<Team, 'id' | 'url'>; team?: Pick<Team, 'id' | 'url'>;
@@ -35,9 +34,9 @@ export const DataTableActionButton = ({ row, team }: DataTableActionButtonProps)
return null; return null;
} }
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email); const recipient = row.recipients.find((recipient) => recipient.email === session.user.email);
const isOwner = row.User.id === session.user.id; const isOwner = row.user.id === session.user.id;
const isRecipient = !!recipient; const isRecipient = !!recipient;
const isDraft = row.status === DocumentStatus.DRAFT; const isDraft = row.status === DocumentStatus.DRAFT;
const isPending = row.status === DocumentStatus.PENDING; const isPending = row.status === DocumentStatus.PENDING;
@@ -50,18 +49,20 @@ export const DataTableActionButton = ({ row, team }: DataTableActionButtonProps)
const onDownloadClick = async () => { const onDownloadClick = async () => {
try { try {
let document: DocumentWithData | null = null; const document = !recipient
? await trpcClient.document.getDocumentById.query(
if (!recipient) { {
document = await trpcClient.document.getDocumentById.query({
documentId: row.id, documentId: row.id,
teamId: team?.id, },
}); {
} else { context: {
document = await trpcClient.document.getDocumentByToken.query({ teamId: team?.id?.toString(),
},
},
)
: await trpcClient.document.getDocumentByToken.query({
token: recipient.token, token: recipient.token,
}); });
}
const documentData = document?.documentData; const documentData = document?.documentData;

View File

@@ -25,7 +25,6 @@ import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { DocumentStatus, RecipientRole } from '@documenso/prisma/client'; import { DocumentStatus, RecipientRole } from '@documenso/prisma/client';
import type { Document, Recipient, Team, User } from '@documenso/prisma/client'; import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import { trpc as trpcClient } from '@documenso/trpc/client'; import { trpc as trpcClient } from '@documenso/trpc/client';
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button'; import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
import { import {
@@ -46,8 +45,8 @@ import { MoveDocumentDialog } from './move-document-dialog';
export type DataTableActionDropdownProps = { export type DataTableActionDropdownProps = {
row: Document & { row: Document & {
User: Pick<User, 'id' | 'name' | 'email'>; user: Pick<User, 'id' | 'name' | 'email'>;
Recipient: Recipient[]; recipients: Recipient[];
team: Pick<Team, 'id' | 'url'> | null; team: Pick<Team, 'id' | 'url'> | null;
}; };
team?: Pick<Team, 'id' | 'url'> & { teamEmail?: string }; team?: Pick<Team, 'id' | 'url'> & { teamEmail?: string };
@@ -66,9 +65,9 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
return null; return null;
} }
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email); const recipient = row.recipients.find((recipient) => recipient.email === session.user.email);
const isOwner = row.User.id === session.user.id; const isOwner = row.user.id === session.user.id;
// const isRecipient = !!recipient; // const isRecipient = !!recipient;
const isDraft = row.status === DocumentStatus.DRAFT; const isDraft = row.status === DocumentStatus.DRAFT;
const isPending = row.status === DocumentStatus.PENDING; const isPending = row.status === DocumentStatus.PENDING;
@@ -81,18 +80,13 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
const onDownloadClick = async () => { const onDownloadClick = async () => {
try { try {
let document: DocumentWithData | null = null; const document = !recipient
? await trpcClient.document.getDocumentById.query({
if (!recipient) {
document = await trpcClient.document.getDocumentById.query({
documentId: row.id, documentId: row.id,
teamId: team?.id, })
}); : await trpcClient.document.getDocumentByToken.query({
} else {
document = await trpcClient.document.getDocumentByToken.query({
token: recipient.token, token: recipient.token,
}); });
}
const documentData = document?.documentData; const documentData = document?.documentData;
@@ -110,7 +104,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
} }
}; };
const nonSignedRecipients = row.Recipient.filter((item) => item.signingStatus !== 'SIGNED'); const nonSignedRecipients = row.recipients.filter((item) => item.signingStatus !== 'SIGNED');
return ( return (
<DropdownMenu> <DropdownMenu>
@@ -195,7 +189,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
{canManageDocument && ( {canManageDocument && (
<DocumentRecipientLinkCopyDialog <DocumentRecipientLinkCopyDialog
recipients={row.Recipient} recipients={row.recipients}
trigger={ trigger={
<DropdownMenuItem disabled={!isPending} asChild onSelect={(e) => e.preventDefault()}> <DropdownMenuItem disabled={!isPending} asChild onSelect={(e) => e.preventDefault()}>
<div> <div>

View File

@@ -25,7 +25,7 @@ export const DataTableSenderFilter = ({ teamId }: DataTableSenderFilterProps) =>
const senderIds = parseToIntegerArray(searchParams?.get('senderIds') ?? ''); const senderIds = parseToIntegerArray(searchParams?.get('senderIds') ?? '');
const { data, isInitialLoading } = trpc.team.getTeamMembers.useQuery({ const { data, isLoading } = trpc.team.getTeamMembers.useQuery({
teamId, teamId,
}); });
@@ -61,7 +61,7 @@ export const DataTableSenderFilter = ({ teamId }: DataTableSenderFilterProps) =>
} }
enableClearAllButton={true} enableClearAllButton={true}
inputPlaceholder={msg`Search`} inputPlaceholder={msg`Search`}
loading={!isMounted || isInitialLoading} loading={!isMounted || isLoading}
options={comboBoxOptions} options={comboBoxOptions}
selectedValues={senderIds} selectedValues={senderIds}
onChange={onChange} onChange={onChange}

View File

@@ -10,9 +10,9 @@ import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
export type DataTableTitleProps = { export type DataTableTitleProps = {
row: Document & { row: Document & {
User: Pick<User, 'id' | 'name' | 'email'>; user: Pick<User, 'id' | 'name' | 'email'>;
team: Pick<Team, 'url'> | null; team: Pick<Team, 'url'> | null;
Recipient: Recipient[]; recipients: Recipient[];
}; };
teamUrl?: string; teamUrl?: string;
}; };
@@ -24,9 +24,9 @@ export const DataTableTitle = ({ row, teamUrl }: DataTableTitleProps) => {
return null; return null;
} }
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email); const recipient = row.recipients.find((recipient) => recipient.email === session.user.email);
const isOwner = row.User.id === session.user.id; const isOwner = row.user.id === session.user.id;
const isRecipient = !!recipient; const isRecipient = !!recipient;
const isCurrentTeamDocument = teamUrl && row.team?.url === teamUrl; const isCurrentTeamDocument = teamUrl && row.team?.url === teamUrl;

View File

@@ -9,9 +9,9 @@ import { DateTime } from 'luxon';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
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 type { TFindDocumentsResponse } from '@documenso/lib/server-only/document/find-documents';
import type { Team } from '@documenso/prisma/client'; import type { Team } from '@documenso/prisma/client';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import type { TFindDocumentsResponse } from '@documenso/trpc/server/document-router/schema';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table'; import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table'; import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
@@ -57,14 +57,14 @@ export const DocumentsDataTable = ({
{ {
id: 'sender', id: 'sender',
header: _(msg`Sender`), header: _(msg`Sender`),
cell: ({ row }) => row.original.User.name ?? row.original.User.email, cell: ({ row }) => row.original.user.name ?? row.original.user.email,
}, },
{ {
header: _(msg`Recipient`), header: _(msg`Recipient`),
accessorKey: 'recipient', accessorKey: 'recipient',
cell: ({ row }) => ( cell: ({ row }) => (
<StackAvatarsWithTooltip <StackAvatarsWithTooltip
recipients={row.original.Recipient} recipients={row.original.recipients}
documentStatus={row.original.status} documentStatus={row.original.status}
/> />
), ),

View File

@@ -38,7 +38,6 @@ export const DeleteDocumentDialog = ({
onOpenChange, onOpenChange,
status, status,
documentTitle, documentTitle,
teamId,
canManageDocument, canManageDocument,
}: DeleteDocumentDialogProps) => { }: DeleteDocumentDialogProps) => {
const router = useRouter(); const router = useRouter();
@@ -52,7 +51,7 @@ export const DeleteDocumentDialog = ({
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT); const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
const { mutateAsync: deleteDocument, isLoading } = trpcReact.document.deleteDocument.useMutation({ const { mutateAsync: deleteDocument, isPending } = trpcReact.document.deleteDocument.useMutation({
onSuccess: () => { onSuccess: () => {
router.refresh(); router.refresh();
void refreshLimits(); void refreshLimits();
@@ -76,7 +75,7 @@ export const DeleteDocumentDialog = ({
const onDelete = async () => { const onDelete = async () => {
try { try {
await deleteDocument({ documentId: id, teamId }); await deleteDocument({ documentId: id });
} catch { } catch {
toast({ toast({
title: _(msg`Something went wrong`), title: _(msg`Something went wrong`),
@@ -93,7 +92,7 @@ export const DeleteDocumentDialog = ({
}; };
return ( return (
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}> <Dialog open={open} onOpenChange={(value) => !isPending && onOpenChange(value)}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
@@ -194,7 +193,7 @@ export const DeleteDocumentDialog = ({
<Button <Button
type="button" type="button"
loading={isLoading} loading={isPending}
onClick={onDelete} onClick={onDelete}
disabled={!isDeleteEnabled && canManageDocument} disabled={!isDeleteEnabled && canManageDocument}
variant="destructive" variant="destructive"

View File

@@ -37,7 +37,6 @@ export const DuplicateDocumentDialog = ({
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({ const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({
documentId: id, documentId: id,
teamId: team?.id,
}); });
const documentData = document?.documentData const documentData = document?.documentData
@@ -49,7 +48,7 @@ export const DuplicateDocumentDialog = ({
const documentsPath = formatDocumentsPath(team?.url); const documentsPath = formatDocumentsPath(team?.url);
const { mutateAsync: duplicateDocument, isLoading: isDuplicateLoading } = const { mutateAsync: duplicateDocument, isPending: isDuplicateLoading } =
trpcReact.document.duplicateDocument.useMutation({ trpcReact.document.duplicateDocument.useMutation({
onSuccess: (newId) => { onSuccess: (newId) => {
router.push(`${documentsPath}/${newId}/edit`); router.push(`${documentsPath}/${newId}/edit`);
@@ -66,7 +65,7 @@ export const DuplicateDocumentDialog = ({
const onDuplicate = async () => { const onDuplicate = async () => {
try { try {
await duplicateDocument({ documentId: id, teamId: team?.id }); await duplicateDocument({ documentId: id });
} catch { } catch {
toast({ toast({
title: _(msg`Something went wrong`), title: _(msg`Something went wrong`),

View File

@@ -42,7 +42,7 @@ export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocum
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery(); const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
const { mutateAsync: moveDocument, isLoading } = trpc.document.moveDocumentToTeam.useMutation({ const { mutateAsync: moveDocument, isPending } = trpc.document.moveDocumentToTeam.useMutation({
onSuccess: () => { onSuccess: () => {
router.refresh(); router.refresh();
toast({ toast({
@@ -119,8 +119,8 @@ export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocum
<Button variant="secondary" onClick={() => onOpenChange(false)}> <Button variant="secondary" onClick={() => onOpenChange(false)}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<Button onClick={onMove} loading={isLoading} disabled={!selectedTeamId || isLoading}> <Button onClick={onMove} loading={isPending} disabled={!selectedTeamId || isPending}>
{isLoading ? <Trans>Moving...</Trans> : <Trans>Move</Trans>} {isPending ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>

View File

@@ -8,16 +8,16 @@ import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
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 { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones'; import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { AppError } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data'; import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone'; import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
@@ -76,7 +76,6 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
const { id } = await createDocument({ const { id } = await createDocument({
title: file.name, title: file.name,
documentDataId, documentDataId,
teamId: team?.id,
timezone: userTimezone, timezone: userTimezone,
}); });
@@ -100,25 +99,20 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
console.error(err); console.error(err);
if (error.code === 'INVALID_DOCUMENT_FILE') { const errorMessage = match(error.code)
toast({ .with('INVALID_DOCUMENT_FILE', () => msg`You cannot upload encrypted PDFs`)
title: _(msg`Invalid file`), .with(
description: _(msg`You cannot upload encrypted PDFs`), AppErrorCode.LIMIT_EXCEEDED,
variant: 'destructive', () => msg`You have reached your document limit for this month. Please upgrade your plan.`,
}); )
} else if (err instanceof TRPCClientError) { .otherwise(() => msg`An error occurred while uploading your document.`);
toast({ toast({
title: _(msg`Error`), title: _(msg`Error`),
description: err.message, description: _(errorMessage),
variant: 'destructive', variant: 'destructive',
duration: 7500,
}); });
} else {
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while uploading your document.`),
variant: 'destructive',
});
}
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }

View File

@@ -36,7 +36,7 @@ export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProp
const [enteredEmail, setEnteredEmail] = useState<string>(''); const [enteredEmail, setEnteredEmail] = useState<string>('');
const { mutateAsync: deleteAccount, isLoading: isDeletingAccount } = const { mutateAsync: deleteAccount, isPending: isDeletingAccount } =
trpc.profile.deleteAccount.useMutation(); trpc.profile.deleteAccount.useMutation();
const onDeleteAccount = async () => { const onDeleteAccount = async () => {

View File

@@ -5,7 +5,6 @@ import { useEffect, useMemo, useState } from 'react';
import { Trans, msg } from '@lingui/macro'; import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import type { FindTemplateRow } from '@documenso/lib/server-only/template/find-templates';
import type { import type {
Team, Team,
TeamProfile, TeamProfile,
@@ -15,6 +14,7 @@ import type {
} from '@documenso/prisma/client'; } from '@documenso/prisma/client';
import { TemplateType } from '@documenso/prisma/client'; import { TemplateType } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { FindTemplateRow } from '@documenso/trpc/server/template-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';
import { Switch } from '@documenso/ui/primitives/switch'; import { Switch } from '@documenso/ui/primitives/switch';
@@ -61,13 +61,12 @@ export const PublicProfilePageView = ({ user, team, profile }: PublicProfilePage
const { data } = trpc.template.findTemplates.useQuery({ const { data } = trpc.template.findTemplates.useQuery({
perPage: 100, perPage: 100,
teamId: team?.id,
}); });
const { mutateAsync: updateUserProfile, isLoading: isUpdatingUserProfile } = const { mutateAsync: updateUserProfile, isPending: isUpdatingUserProfile } =
trpc.profile.updatePublicProfile.useMutation(); trpc.profile.updatePublicProfile.useMutation();
const { mutateAsync: updateTeamProfile, isLoading: isUpdatingTeamProfile } = const { mutateAsync: updateTeamProfile, isPending: isUpdatingTeamProfile } =
trpc.team.updateTeamPublicProfile.useMutation(); trpc.team.updateTeamPublicProfile.useMutation();
const isUpdating = isUpdatingUserProfile || isUpdatingTeamProfile; const isUpdating = isUpdatingUserProfile || isUpdatingTeamProfile;

View File

@@ -7,11 +7,11 @@ import { useLingui } from '@lingui/react';
import { EditIcon, FileIcon, LinkIcon, MoreHorizontalIcon, Trash2Icon } from 'lucide-react'; import { EditIcon, FileIcon, LinkIcon, MoreHorizontalIcon, Trash2Icon } from 'lucide-react';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard'; import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
import type { FindTemplateRow } from '@documenso/lib/server-only/template/find-templates';
import { formatDirectTemplatePath } from '@documenso/lib/utils/templates'; import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
import type { TemplateDirectLink } from '@documenso/prisma/client'; import type { TemplateDirectLink } from '@documenso/prisma/client';
import { TemplateType } from '@documenso/prisma/client'; import { TemplateType } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { FindTemplateRow } from '@documenso/trpc/server/template-router/schema';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -23,15 +23,12 @@ import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { ManagePublicTemplateDialog } from '~/components/templates/manage-public-template-dialog'; import { ManagePublicTemplateDialog } from '~/components/templates/manage-public-template-dialog';
import { useOptionalCurrentTeam } from '~/providers/team';
type DirectTemplate = FindTemplateRow & { type DirectTemplate = FindTemplateRow & {
directLink: Pick<TemplateDirectLink, 'token' | 'enabled'>; directLink: Pick<TemplateDirectLink, 'token' | 'enabled'>;
}; };
export const PublicTemplatesDataTable = () => { export const PublicTemplatesDataTable = () => {
const team = useOptionalCurrentTeam();
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
@@ -42,12 +39,10 @@ export const PublicTemplatesDataTable = () => {
templateId: number; templateId: number;
} | null>(null); } | null>(null);
const { data, isInitialLoading, isLoadingError, refetch } = trpc.template.findTemplates.useQuery( const { data, isLoading, isLoadingError, refetch } = trpc.template.findTemplates.useQuery(
{},
{ {
teamId: team?.id, placeholderData: (previousData) => previousData,
},
{
keepPreviousData: true,
}, },
); );
@@ -85,7 +80,7 @@ export const PublicTemplatesDataTable = () => {
{/* Loading and error handling states. */} {/* Loading and error handling states. */}
{publicDirectTemplates.length === 0 && ( {publicDirectTemplates.length === 0 && (
<> <>
{isInitialLoading && {isLoading &&
Array(3) Array(3)
.fill(0) .fill(0)
.map((_, index) => ( .map((_, index) => (
@@ -120,7 +115,7 @@ export const PublicTemplatesDataTable = () => {
</div> </div>
)} )}
{!isInitialLoading && ( {!isLoading && (
<div className="text-muted-foreground flex h-32 flex-col items-center justify-center text-sm"> <div className="text-muted-foreground flex h-32 flex-col items-center justify-center text-sm">
<Trans>No public profile templates found</Trans> <Trans>No public profile templates found</Trans>
<ManagePublicTemplateDialog <ManagePublicTemplateDialog

View File

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

View File

@@ -65,7 +65,7 @@ export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePass
}, },
}); });
const { mutateAsync: createPasskeyRegistrationOptions, isLoading } = const { mutateAsync: createPasskeyRegistrationOptions, isPending } =
trpc.auth.createPasskeyRegistrationOptions.useMutation(); trpc.auth.createPasskeyRegistrationOptions.useMutation();
const { mutateAsync: createPasskey } = trpc.auth.createPasskey.useMutation(); const { mutateAsync: createPasskey } = trpc.auth.createPasskey.useMutation();
@@ -141,7 +141,7 @@ export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePass
> >
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}> <DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? ( {trigger ?? (
<Button variant="secondary" loading={isLoading}> <Button variant="secondary" loading={isPending}>
<KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" /> <KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />
<Trans>Add passkey</Trans> <Trans>Add passkey</Trans>
</Button> </Button>

View File

@@ -60,7 +60,7 @@ export const UserPasskeysDataTableActions = ({
}, },
}); });
const { mutateAsync: updatePasskey, isLoading: isUpdatingPasskey } = const { mutateAsync: updatePasskey, isPending: isUpdatingPasskey } =
trpc.auth.updatePasskey.useMutation({ trpc.auth.updatePasskey.useMutation({
onSuccess: () => { onSuccess: () => {
toast({ toast({
@@ -80,7 +80,7 @@ export const UserPasskeysDataTableActions = ({
}, },
}); });
const { mutateAsync: deletePasskey, isLoading: isDeletingPasskey } = const { mutateAsync: deletePasskey, isPending: isDeletingPasskey } =
trpc.auth.deletePasskey.useMutation({ trpc.auth.deletePasskey.useMutation({
onSuccess: () => { onSuccess: () => {
toast({ toast({

View File

@@ -29,13 +29,13 @@ export const UserPasskeysDataTable = () => {
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? [])); const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
const { data, isLoading, isInitialLoading, isLoadingError } = trpc.auth.findPasskeys.useQuery( const { data, isLoading, isLoadingError } = trpc.auth.findPasskeys.useQuery(
{ {
page: parsedSearchParams.page, page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage, perPage: parsedSearchParams.perPage,
}, },
{ {
keepPreviousData: true, placeholderData: (previousData) => previousData,
}, },
); );
@@ -100,7 +100,7 @@ export const UserPasskeysDataTable = () => {
enable: isLoadingError, enable: isLoadingError,
}} }}
skeleton={{ skeleton={{
enable: isLoading && isInitialLoading, enable: isLoading,
rows: 3, rows: 3,
component: ( component: (
<> <>

View File

@@ -17,7 +17,7 @@ export const AcceptTeamInvitationButton = ({ teamId }: AcceptTeamInvitationButto
const { const {
mutateAsync: acceptTeamInvitation, mutateAsync: acceptTeamInvitation,
isLoading, isPending,
isSuccess, isSuccess,
} = trpc.team.acceptTeamInvitation.useMutation({ } = trpc.team.acceptTeamInvitation.useMutation({
onSuccess: () => { onSuccess: () => {
@@ -40,8 +40,8 @@ export const AcceptTeamInvitationButton = ({ teamId }: AcceptTeamInvitationButto
return ( return (
<Button <Button
onClick={async () => acceptTeamInvitation({ teamId })} onClick={async () => acceptTeamInvitation({ teamId })}
loading={isLoading} loading={isPending}
disabled={isLoading || isSuccess} disabled={isPending || isSuccess}
> >
<Trans>Accept</Trans> <Trans>Accept</Trans>
</Button> </Button>

View File

@@ -17,7 +17,7 @@ export const DeclineTeamInvitationButton = ({ teamId }: DeclineTeamInvitationBut
const { const {
mutateAsync: declineTeamInvitation, mutateAsync: declineTeamInvitation,
isLoading, isPending,
isSuccess, isSuccess,
} = trpc.team.declineTeamInvitation.useMutation({ } = trpc.team.declineTeamInvitation.useMutation({
onSuccess: () => { onSuccess: () => {
@@ -40,8 +40,8 @@ export const DeclineTeamInvitationButton = ({ teamId }: DeclineTeamInvitationBut
return ( return (
<Button <Button
onClick={async () => declineTeamInvitation({ teamId })} onClick={async () => declineTeamInvitation({ teamId })}
loading={isLoading} loading={isPending}
disabled={isLoading || isSuccess} disabled={isPending || isSuccess}
variant="ghost" variant="ghost"
> >
<Trans>Decline</Trans> <Trans>Decline</Trans>

View File

@@ -30,7 +30,7 @@ export const TeamEmailUsage = ({ teamEmail }: TeamEmailUsageProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { mutateAsync: deleteTeamEmail, isLoading: isDeletingTeamEmail } = const { mutateAsync: deleteTeamEmail, isPending: isDeletingTeamEmail } =
trpc.team.deleteTeamEmail.useMutation({ trpc.team.deleteTeamEmail.useMutation({
onSuccess: () => { onSuccess: () => {
toast({ toast({

View File

@@ -23,11 +23,11 @@ import { AcceptTeamInvitationButton } from './accept-team-invitation-button';
import { DeclineTeamInvitationButton } from './decline-team-invitation-button'; import { DeclineTeamInvitationButton } from './decline-team-invitation-button';
export const TeamInvitations = () => { export const TeamInvitations = () => {
const { data, isInitialLoading } = trpc.team.getTeamInvitations.useQuery(); const { data, isLoading } = trpc.team.getTeamInvitations.useQuery();
return ( return (
<AnimatePresence> <AnimatePresence>
{data && data.length > 0 && !isInitialLoading && ( {data && data.length > 0 && !isLoading && (
<AnimateGenericFadeInOut> <AnimateGenericFadeInOut>
<Alert variant="secondary"> <Alert variant="secondary">
<div className="flex h-full flex-row items-center p-2"> <div className="flex h-full flex-row items-center p-2">

View File

@@ -12,7 +12,7 @@ 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 type { TemplateWithDetails } from '@documenso/prisma/types/template'; import type { TTemplate } from '@documenso/lib/types/template';
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 { Card, CardContent } from '@documenso/ui/primitives/card'; import { Card, CardContent } from '@documenso/ui/primitives/card';
@@ -32,7 +32,7 @@ import { useOptionalCurrentTeam } from '~/providers/team';
export type EditTemplateFormProps = { export type EditTemplateFormProps = {
className?: string; className?: string;
initialTemplate: TemplateWithDetails; initialTemplate: TTemplate;
isEnterprise: boolean; isEnterprise: boolean;
templateRootPath: string; templateRootPath: string;
}; };
@@ -62,7 +62,6 @@ export const EditTemplateForm = ({
const { data: template, refetch: refetchTemplate } = trpc.template.getTemplateById.useQuery( const { data: template, refetch: refetchTemplate } = trpc.template.getTemplateById.useQuery(
{ {
templateId: initialTemplate.id, templateId: initialTemplate.id,
teamId: initialTemplate.teamId || undefined,
}, },
{ {
initialData: initialTemplate, initialData: initialTemplate,
@@ -70,7 +69,7 @@ export const EditTemplateForm = ({
}, },
); );
const { Recipient: recipients, Field: fields, templateDocumentData } = template; const { recipients, fields, templateDocumentData } = template;
const documentFlow: Record<EditTemplateStep, DocumentFlowStep> = { const documentFlow: Record<EditTemplateStep, DocumentFlowStep> = {
settings: { settings: {
@@ -104,19 +103,6 @@ export const EditTemplateForm = ({
}, },
}); });
const { mutateAsync: setSigningOrderForTemplate } =
trpc.template.setSigningOrderForTemplate.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.template.getTemplateById.setData(
{
templateId: initialTemplate.id,
},
(oldData) => ({ ...(oldData || initialTemplate), ...newData }),
);
},
});
const { mutateAsync: addTemplateFields } = trpc.field.addTemplateFields.useMutation({ const { mutateAsync: addTemplateFields } = trpc.field.addTemplateFields.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => { onSuccess: (newData) => {
@@ -129,7 +115,7 @@ export const EditTemplateForm = ({
}, },
}); });
const { mutateAsync: addTemplateSigners } = trpc.recipient.addTemplateSigners.useMutation({ const { mutateAsync: setRecipients } = trpc.recipient.setTemplateRecipients.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => { onSuccess: (newData) => {
utils.template.getTemplateById.setData( utils.template.getTemplateById.setData(
@@ -141,31 +127,14 @@ export const EditTemplateForm = ({
}, },
}); });
const { mutateAsync: updateTypedSignature } =
trpc.template.updateTemplateTypedSignatureSettings.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.template.getTemplateById.setData(
{
templateId: initialTemplate.id,
},
(oldData) => ({
...(oldData || initialTemplate),
...newData,
id: Number(newData.id),
}),
);
},
});
const onAddSettingsFormSubmit = async (data: TAddTemplateSettingsFormSchema) => { const onAddSettingsFormSubmit = async (data: TAddTemplateSettingsFormSchema) => {
try { try {
await updateTemplateSettings({ await updateTemplateSettings({
templateId: template.id, templateId: template.id,
teamId: team?.id,
data: { data: {
title: data.title, title: data.title,
externalId: data.externalId || null, externalId: data.externalId || null,
visibility: data.visibility,
globalAccessAuth: data.globalAccessAuth ?? null, globalAccessAuth: data.globalAccessAuth ?? null,
globalActionAuth: data.globalActionAuth ?? null, globalActionAuth: data.globalActionAuth ?? null,
}, },
@@ -195,16 +164,16 @@ export const EditTemplateForm = ({
) => { ) => {
try { try {
await Promise.all([ await Promise.all([
setSigningOrderForTemplate({ updateTemplateSettings({
templateId: template.id, templateId: template.id,
teamId: team?.id, meta: {
signingOrder: data.signingOrder, signingOrder: data.signingOrder,
},
}), }),
addTemplateSigners({ setRecipients({
templateId: template.id, templateId: template.id,
teamId: team?.id, recipients: data.signers,
signers: data.signers,
}), }),
]); ]);
@@ -228,10 +197,11 @@ export const EditTemplateForm = ({
fields: data.fields, fields: data.fields,
}); });
await updateTypedSignature({ await updateTemplateSettings({
templateId: template.id, templateId: template.id,
teamId: team?.id, meta: {
typedSignatureEnabled: data.typedSignatureEnabled, typedSignatureEnabled: data.typedSignatureEnabled,
},
}); });
// Clear all field data from localStorage // Clear all field data from localStorage
@@ -296,6 +266,7 @@ export const EditTemplateForm = ({
<AddTemplateSettingsFormPartial <AddTemplateSettingsFormPartial
key={recipients.length} key={recipients.length}
template={template} template={template}
currentTeamMemberRole={team?.currentTeamMember?.role}
documentFlow={documentFlow.settings} documentFlow={documentFlow.settings}
recipients={recipients} recipients={recipients}
fields={fields} fields={fields}

View File

@@ -11,7 +11,7 @@ import { Button } from '@documenso/ui/primitives/button';
import { TemplateDirectLinkDialog } from '../template-direct-link-dialog'; import { TemplateDirectLinkDialog } from '../template-direct-link-dialog';
export type TemplateDirectLinkDialogWrapperProps = { export type TemplateDirectLinkDialogWrapperProps = {
template: Template & { directLink?: TemplateDirectLink | null; Recipient: Recipient[] }; template: Template & { directLink?: TemplateDirectLink | null; recipients: Recipient[] };
}; };
export const TemplateDirectLinkDialogWrapper = ({ export const TemplateDirectLinkDialogWrapper = ({

View File

@@ -69,11 +69,9 @@ export const TemplatePageViewDocumentsTable = ({
Object.fromEntries(searchParams ?? []), Object.fromEntries(searchParams ?? []),
); );
const { data, isLoading, isInitialLoading, isLoadingError } = const { data, isLoading, isLoadingError } = trpc.document.findDocuments.useQuery(
trpc.document.findDocuments.useQuery(
{ {
templateId, templateId,
teamId: team?.id,
page: parsedSearchParams.page, page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage, perPage: parsedSearchParams.perPage,
query: parsedSearchParams.query, query: parsedSearchParams.query,
@@ -81,7 +79,7 @@ export const TemplatePageViewDocumentsTable = ({
status: parsedSearchParams.status, status: parsedSearchParams.status,
}, },
{ {
keepPreviousData: true, placeholderData: (previousData) => previousData,
}, },
); );
@@ -117,7 +115,7 @@ export const TemplatePageViewDocumentsTable = ({
accessorKey: 'recipient', accessorKey: 'recipient',
cell: ({ row }) => ( cell: ({ row }) => (
<StackAvatarsWithTooltip <StackAvatarsWithTooltip
recipients={row.original.Recipient} recipients={row.original.recipients}
documentStatus={row.original.status} documentStatus={row.original.status}
/> />
), ),
@@ -242,7 +240,7 @@ export const TemplatePageViewDocumentsTable = ({
enable: isLoadingError, enable: isLoadingError,
}} }}
skeleton={{ skeleton={{
enable: isLoading && isInitialLoading, enable: isLoading,
rows: 3, rows: 3,
component: ( component: (
<> <>

View File

@@ -12,7 +12,7 @@ import type { Template, User } from '@documenso/prisma/client';
export type TemplatePageViewInformationProps = { export type TemplatePageViewInformationProps = {
userId: number; userId: number;
template: Template & { template: Template & {
User: Pick<User, 'id' | 'name' | 'email'>; user: Pick<User, 'id' | 'name' | 'email'>;
}; };
}; };
@@ -28,7 +28,8 @@ export const TemplatePageViewInformation = ({
return [ return [
{ {
description: msg`Uploaded by`, description: msg`Uploaded by`,
value: userId === template.userId ? _(msg`You`) : template.User.name ?? template.User.email, value:
userId === template.userId ? _(msg`You`) : (template.user.name ?? template.user.email),
}, },
{ {
description: msg`Created`, description: msg`Created`,

View File

@@ -20,12 +20,10 @@ export type TemplatePageViewRecentActivityProps = {
export const TemplatePageViewRecentActivity = ({ export const TemplatePageViewRecentActivity = ({
templateId, templateId,
teamId,
documentRootPath, documentRootPath,
}: TemplatePageViewRecentActivityProps) => { }: TemplatePageViewRecentActivityProps) => {
const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocuments.useQuery({ const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocuments.useQuery({
templateId, templateId,
teamId,
orderByColumn: 'createdAt', orderByColumn: 'createdAt',
orderByDirection: 'asc', orderByDirection: 'asc',
perPage: 5, perPage: 5,
@@ -125,7 +123,7 @@ export const TemplatePageViewRecentActivity = ({
{match(document.source) {match(document.source)
.with(DocumentSource.DOCUMENT, DocumentSource.TEMPLATE, () => ( .with(DocumentSource.DOCUMENT, DocumentSource.TEMPLATE, () => (
<Trans> <Trans>
Document created by <span className="font-bold">{document.User.name}</span> Document created by <span className="font-bold">{document.user.name}</span>
</Trans> </Trans>
)) ))
.with(DocumentSource.TEMPLATE_DIRECT_LINK, () => ( .with(DocumentSource.TEMPLATE_DIRECT_LINK, () => (

View File

@@ -10,7 +10,7 @@ import { AvatarWithText } from '@documenso/ui/primitives/avatar';
export type TemplatePageViewRecipientsProps = { export type TemplatePageViewRecipientsProps = {
template: Template & { template: Template & {
Recipient: Recipient[]; recipients: Recipient[];
}; };
templateRootPath: string; templateRootPath: string;
}; };
@@ -21,7 +21,7 @@ export const TemplatePageViewRecipients = ({
}: TemplatePageViewRecipientsProps) => { }: TemplatePageViewRecipientsProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const recipients = template.Recipient; const recipients = template.recipients;
return ( return (
<section className="dark:bg-background border-border bg-widget flex flex-col rounded-xl border"> <section className="dark:bg-background border-border bg-widget flex flex-col rounded-xl border">

View File

@@ -54,10 +54,10 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
redirect(templateRootPath); redirect(templateRootPath);
} }
const { templateDocumentData, Field, Recipient: recipients, templateMeta } = template; const { templateDocumentData, fields, recipients, templateMeta } = template;
// Remap to fit the DocumentReadOnlyFields component. // Remap to fit the DocumentReadOnlyFields component.
const readOnlyFields = Field.map((field) => { const readOnlyFields = fields.map((field) => {
const recipient = recipients.find((recipient) => recipient.id === field.recipientId) || { const recipient = recipients.find((recipient) => recipient.id === field.recipientId) || {
name: '', name: '',
email: '', email: '',
@@ -66,8 +66,8 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
return { return {
...field, ...field,
Recipient: recipient, recipient,
Signature: null, signature: null,
}; };
}); });
@@ -165,7 +165,7 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
<UseTemplateDialog <UseTemplateDialog
templateId={template.id} templateId={template.id}
templateSigningOrder={template.templateMeta?.signingOrder} templateSigningOrder={template.templateMeta?.signingOrder}
recipients={template.Recipient} recipients={template.recipients}
documentRootPath={documentRootPath} documentRootPath={documentRootPath}
trigger={ trigger={
<Button className="w-full"> <Button className="w-full">

View File

@@ -25,7 +25,7 @@ import { TemplateDirectLinkDialog } from './template-direct-link-dialog';
export type DataTableActionDropdownProps = { export type DataTableActionDropdownProps = {
row: Template & { row: Template & {
directLink?: Pick<TemplateDirectLink, 'token' | 'enabled'> | null; directLink?: Pick<TemplateDirectLink, 'token' | 'enabled'> | null;
Recipient: Recipient[]; recipients: Recipient[];
}; };
templateRootPath: string; templateRootPath: string;
teamId?: number; teamId?: number;

View File

@@ -10,7 +10,7 @@ import { AlertTriangle, Globe2Icon, InfoIcon, Link2Icon, Loader, LockIcon } from
import { useLimits } from '@documenso/ee/server-only/limits/provider/client'; import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
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 type { FindTemplateRow } from '@documenso/lib/server-only/template/find-templates'; import type { FindTemplateRow } from '@documenso/trpc/server/template-router/schema';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table'; import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table'; import { DataTable } from '@documenso/ui/primitives/data-table';
@@ -146,7 +146,7 @@ export const TemplatesDataTable = ({
templateId={row.original.id} templateId={row.original.id}
templateSigningOrder={row.original.templateMeta?.signingOrder} templateSigningOrder={row.original.templateMeta?.signingOrder}
documentDistributionMethod={row.original.templateMeta?.distributionMethod} documentDistributionMethod={row.original.templateMeta?.distributionMethod}
recipients={row.original.Recipient} recipients={row.original.recipients}
documentRootPath={documentRootPath} documentRootPath={documentRootPath}
/> />

View File

@@ -22,18 +22,13 @@ type DeleteTemplateDialogProps = {
onOpenChange: (_open: boolean) => void; onOpenChange: (_open: boolean) => void;
}; };
export const DeleteTemplateDialog = ({ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateDialogProps) => {
id,
teamId,
open,
onOpenChange,
}: DeleteTemplateDialogProps) => {
const router = useRouter(); const router = useRouter();
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { mutateAsync: deleteTemplate, isLoading } = trpcReact.template.deleteTemplate.useMutation({ const { mutateAsync: deleteTemplate, isPending } = trpcReact.template.deleteTemplate.useMutation({
onSuccess: () => { onSuccess: () => {
router.refresh(); router.refresh();
@@ -56,7 +51,7 @@ export const DeleteTemplateDialog = ({
}); });
return ( return (
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}> <Dialog open={open} onOpenChange={(value) => !isPending && onOpenChange(value)}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
@@ -75,7 +70,7 @@ export const DeleteTemplateDialog = ({
<Button <Button
type="button" type="button"
variant="secondary" variant="secondary"
disabled={isLoading} disabled={isPending}
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
> >
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
@@ -84,8 +79,8 @@ export const DeleteTemplateDialog = ({
<Button <Button
type="button" type="button"
variant="destructive" variant="destructive"
loading={isLoading} loading={isPending}
onClick={async () => deleteTemplate({ templateId: id, teamId })} onClick={async () => deleteTemplate({ templateId: id })}
> >
<Trans>Delete</Trans> <Trans>Delete</Trans>
</Button> </Button>

View File

@@ -24,7 +24,6 @@ type DuplicateTemplateDialogProps = {
export const DuplicateTemplateDialog = ({ export const DuplicateTemplateDialog = ({
id, id,
teamId,
open, open,
onOpenChange, onOpenChange,
}: DuplicateTemplateDialogProps) => { }: DuplicateTemplateDialogProps) => {
@@ -33,7 +32,7 @@ export const DuplicateTemplateDialog = ({
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { mutateAsync: duplicateTemplate, isLoading } = const { mutateAsync: duplicateTemplate, isPending } =
trpcReact.template.duplicateTemplate.useMutation({ trpcReact.template.duplicateTemplate.useMutation({
onSuccess: () => { onSuccess: () => {
router.refresh(); router.refresh();
@@ -56,7 +55,7 @@ export const DuplicateTemplateDialog = ({
}); });
return ( return (
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}> <Dialog open={open} onOpenChange={(value) => !isPending && onOpenChange(value)}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
@@ -71,7 +70,7 @@ export const DuplicateTemplateDialog = ({
<DialogFooter> <DialogFooter>
<Button <Button
type="button" type="button"
disabled={isLoading} disabled={isPending}
variant="secondary" variant="secondary"
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
> >
@@ -80,11 +79,10 @@ export const DuplicateTemplateDialog = ({
<Button <Button
type="button" type="button"
loading={isLoading} loading={isPending}
onClick={async () => onClick={async () =>
duplicateTemplate({ duplicateTemplate({
templateId: id, templateId: id,
teamId,
}) })
} }
> >

View File

@@ -43,7 +43,7 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null); const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery(); const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
const { mutateAsync: moveTemplate, isLoading } = trpc.template.moveTemplateToTeam.useMutation({ const { mutateAsync: moveTemplate, isPending } = trpc.template.moveTemplateToTeam.useMutation({
onSuccess: () => { onSuccess: () => {
router.refresh(); router.refresh();
toast({ toast({
@@ -130,8 +130,8 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
<Button variant="secondary" onClick={() => onOpenChange(false)}> <Button variant="secondary" onClick={() => onOpenChange(false)}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<Button onClick={onMove} loading={isLoading} disabled={!selectedTeamId || isLoading}> <Button onClick={onMove} loading={isPending} disabled={!selectedTeamId || isPending}>
{isLoading ? <Trans>Moving...</Trans> : <Trans>Move</Trans>} {isPending ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>

View File

@@ -31,7 +31,7 @@ type NewTemplateDialogProps = {
templateRootPath: string; templateRootPath: string;
}; };
export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialogProps) => { export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps) => {
const router = useRouter(); const router = useRouter();
const { data: session } = useSession(); const { data: session } = useSession();
@@ -58,7 +58,6 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo
}); });
const { id } = await createTemplate({ const { id } = await createTemplate({
teamId,
title: file.name, title: file.name,
templateDocumentDataId, templateDocumentDataId,
}); });

View File

@@ -46,12 +46,10 @@ import {
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
type TemplateDirectLinkDialogProps = { type TemplateDirectLinkDialogProps = {
template: Template & { template: Template & {
directLink?: Pick<TemplateDirectLink, 'token' | 'enabled'> | null; directLink?: Pick<TemplateDirectLink, 'token' | 'enabled'> | null;
Recipient: Recipient[]; recipients: Recipient[];
}; };
open: boolean; open: boolean;
onOpenChange: (_open: boolean) => void; onOpenChange: (_open: boolean) => void;
@@ -68,8 +66,6 @@ export const TemplateDirectLinkDialog = ({
const { quota, remaining } = useLimits(); const { quota, remaining } = useLimits();
const { _ } = useLingui(); const { _ } = useLingui();
const team = useOptionalCurrentTeam();
const [, copy] = useCopyToClipboard(); const [, copy] = useCopyToClipboard();
const router = useRouter(); const router = useRouter();
@@ -81,13 +77,13 @@ export const TemplateDirectLinkDialog = ({
); );
const validDirectTemplateRecipients = useMemo( const validDirectTemplateRecipients = useMemo(
() => template.Recipient.filter((recipient) => recipient.role !== RecipientRole.CC), () => template.recipients.filter((recipient) => recipient.role !== RecipientRole.CC),
[template.Recipient], [template.recipients],
); );
const { const {
mutateAsync: createTemplateDirectLink, mutateAsync: createTemplateDirectLink,
isLoading: isCreatingTemplateDirectLink, isPending: isCreatingTemplateDirectLink,
reset: resetCreateTemplateDirectLink, reset: resetCreateTemplateDirectLink,
} = trpcReact.template.createTemplateDirectLink.useMutation({ } = trpcReact.template.createTemplateDirectLink.useMutation({
onSuccess: (data) => { onSuccess: (data) => {
@@ -108,7 +104,7 @@ export const TemplateDirectLinkDialog = ({
}, },
}); });
const { mutateAsync: toggleTemplateDirectLink, isLoading: isTogglingTemplateAccess } = const { mutateAsync: toggleTemplateDirectLink, isPending: isTogglingTemplateAccess } =
trpcReact.template.toggleTemplateDirectLink.useMutation({ trpcReact.template.toggleTemplateDirectLink.useMutation({
onSuccess: (data) => { onSuccess: (data) => {
const enabledDescription = msg`Direct link signing has been enabled`; const enabledDescription = msg`Direct link signing has been enabled`;
@@ -131,7 +127,7 @@ export const TemplateDirectLinkDialog = ({
}, },
}); });
const { mutateAsync: deleteTemplateDirectLink, isLoading: isDeletingTemplateDirectLink } = const { mutateAsync: deleteTemplateDirectLink, isPending: isDeletingTemplateDirectLink } =
trpcReact.template.deleteTemplateDirectLink.useMutation({ trpcReact.template.deleteTemplateDirectLink.useMutation({
onSuccess: () => { onSuccess: () => {
onOpenChange(false); onOpenChange(false);
@@ -174,7 +170,6 @@ export const TemplateDirectLinkDialog = ({
await createTemplateDirectLink({ await createTemplateDirectLink({
templateId: template.id, templateId: template.id,
teamId: team?.id,
directRecipientId: recipientId, directRecipientId: recipientId,
}); });
}; };
@@ -327,7 +322,7 @@ export const TemplateDirectLinkDialog = ({
</div> </div>
{/* Prevent creating placeholder direct template recipient if the email already exists. */} {/* Prevent creating placeholder direct template recipient if the email already exists. */}
{!template.Recipient.some( {!template.recipients.some(
(recipient) => recipient.email === DIRECT_TEMPLATE_RECIPIENT_EMAIL, (recipient) => recipient.email === DIRECT_TEMPLATE_RECIPIENT_EMAIL,
) && ( ) && (
<DialogFooter className="mx-auto"> <DialogFooter className="mx-auto">
@@ -345,7 +340,6 @@ export const TemplateDirectLinkDialog = ({
onClick={async () => onClick={async () =>
createTemplateDirectLink({ createTemplateDirectLink({
templateId: template.id, templateId: template.id,
teamId: team?.id,
}) })
} }
> >

View File

@@ -7,15 +7,17 @@ import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { InfoIcon, Plus } 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 * as z from 'zod'; import * as z from 'zod';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { import {
TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX, TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX,
TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX, TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX,
} 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 type { Recipient } from '@documenso/prisma/client'; import type { Recipient } from '@documenso/prisma/client';
import { DocumentDistributionMethod, DocumentSigningOrder } from '@documenso/prisma/client'; import { DocumentDistributionMethod, DocumentSigningOrder } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
@@ -45,11 +47,14 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitive
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';
import { useOptionalCurrentTeam } from '~/providers/team';
const ZAddRecipientsForNewDocumentSchema = z const ZAddRecipientsForNewDocumentSchema = z
.object({ .object({
distributeDocument: z.boolean(), distributeDocument: z.boolean(),
useCustomDocument: z.boolean().default(false),
customDocumentData: z
.any()
.refine((data) => data instanceof File || data === undefined)
.optional(),
recipients: z.array( recipients: z.array(
z.object({ z.object({
id: z.number(), id: z.number(),
@@ -113,12 +118,12 @@ export function UseTemplateDialog({
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const team = useOptionalCurrentTeam();
const form = useForm<TAddRecipientsForNewDocumentSchema>({ const form = useForm<TAddRecipientsForNewDocumentSchema>({
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema), resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
defaultValues: { defaultValues: {
distributeDocument: false, distributeDocument: false,
useCustomDocument: false,
customDocumentData: undefined,
recipients: recipients recipients: recipients
.sort((a, b) => (a.signingOrder || 0) - (b.signingOrder || 0)) .sort((a, b) => (a.signingOrder || 0) - (b.signingOrder || 0))
.map((recipient) => { .map((recipient) => {
@@ -145,11 +150,18 @@ export function UseTemplateDialog({
const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => { const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => {
try { try {
let customDocumentDataId: string | undefined = undefined;
if (data.useCustomDocument && data.customDocumentData) {
const customDocumentData = await putPdfFile(data.customDocumentData);
customDocumentDataId = customDocumentData.id;
}
const { id } = await createDocumentFromTemplate({ const { id } = await createDocumentFromTemplate({
templateId, templateId,
teamId: team?.id,
recipients: data.recipients, recipients: data.recipients,
distributeDocument: data.distributeDocument, distributeDocument: data.distributeDocument,
customDocumentDataId,
}); });
toast({ toast({
@@ -300,7 +312,6 @@ export function UseTemplateDialog({
/> />
</div> </div>
))} ))}
</div>
{recipients.length > 0 && ( {recipients.length > 0 && (
<div className="mt-4 flex flex-row items-center"> <div className="mt-4 flex flex-row items-center">
@@ -331,8 +342,8 @@ export function UseTemplateDialog({
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4"> <TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
<p> <p>
<Trans> <Trans>
The document will be immediately sent to recipients if this is The document will be immediately sent to recipients if this
checked. is checked.
</Trans> </Trans>
</p> </p>
@@ -358,7 +369,9 @@ export function UseTemplateDialog({
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4"> <TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
<p> <p>
<Trans>Create the document as pending and ready to sign.</Trans> <Trans>
Create the document as pending and ready to sign.
</Trans>
</p> </p>
<p> <p>
@@ -367,8 +380,8 @@ export function UseTemplateDialog({
<p className="mt-2"> <p className="mt-2">
<Trans> <Trans>
We will generate signing links for you, which you can send to We will generate signing links for you, which you can send
the recipients through your method of choice. to the recipients through your method of choice.
</Trans> </Trans>
</p> </p>
</TooltipContent> </TooltipContent>
@@ -382,7 +395,162 @@ export function UseTemplateDialog({
</div> </div>
)} )}
<DialogFooter> <FormField
control={form.control}
name="useCustomDocument"
render={({ field }) => (
<FormItem>
<div className="flex flex-row items-center">
<Checkbox
id="useCustomDocument"
className="h-5 w-5"
checked={field.value}
onCheckedChange={(checked) => {
field.onChange(checked);
if (!checked) {
form.setValue('customDocumentData', undefined);
}
}}
/>
<label
className="text-muted-foreground ml-2 flex items-center text-sm"
htmlFor="useCustomDocument"
>
<Trans>Upload custom document</Trans>
<Tooltip>
<TooltipTrigger type="button">
<InfoIcon className="mx-1 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
<p>
<Trans>
Upload a custom document to use instead of the template's default
document
</Trans>
</p>
</TooltipContent>
</Tooltip>
</label>
</div>
</FormItem>
)}
/>
{form.watch('useCustomDocument') && (
<div className="my-4">
<FormField
control={form.control}
name="customDocumentData"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="w-full space-y-4">
<label
className={cn(
'text-muted-foreground hover:border-muted-foreground/50 group relative flex min-h-[150px] cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed border-gray-300 px-6 py-10 transition-colors',
{
'border-destructive hover:border-destructive':
form.formState.errors.customDocumentData,
},
)}
>
<div className="text-center">
{!field.value && (
<>
<Upload className="text-muted-foreground/50 mx-auto h-10 w-10" />
<div className="mt-4 flex text-sm leading-6">
<span className="text-muted-foreground relative">
<Trans>
<span className="text-primary font-semibold">
Click to upload
</span>{' '}
or drag and drop
</Trans>
</span>
</div>
<p className="text-muted-foreground/80 text-xs">
PDF files only
</p>
</>
)}
{field.value && (
<div className="text-muted-foreground space-y-1">
<p className="text-sm font-medium">{field.value.name}</p>
<p className="text-muted-foreground/60 text-xs">
{(field.value.size / (1024 * 1024)).toFixed(2)} MB
</p>
</div>
)}
</div>
<input
type="file"
className="absolute h-full w-full opacity-0"
accept=".pdf,application/pdf"
onChange={(e) => {
const file = e.target.files?.[0];
if (!file) {
field.onChange(undefined);
return;
}
if (file.type !== 'application/pdf') {
form.setError('customDocumentData', {
type: 'manual',
message: _(msg`Please select a PDF file`),
});
return;
}
if (file.size > APP_DOCUMENT_UPLOAD_SIZE_LIMIT * 1024 * 1024) {
form.setError('customDocumentData', {
type: 'manual',
message: _(
msg`File size exceeds the limit of ${APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB`,
),
});
return;
}
field.onChange(file);
}}
/>
{field.value && (
<div className="absolute right-2 top-2">
<Button
type="button"
variant="destructive"
className="h-6 w-6 p-0"
onClick={(e) => {
e.preventDefault();
field.onChange(undefined);
}}
>
<X className="h-4 w-4" />
<div className="sr-only">
<Trans>Clear file</Trans>
</div>
</Button>
</div>
)}
</label>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
</div>
<DialogFooter className="mt-4">
<DialogClose asChild> <DialogClose asChild>
<Button type="button" variant="secondary"> <Button type="button" variant="secondary">
<Trans>Close</Trans> <Trans>Close</Trans>

View File

@@ -66,6 +66,7 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
const { data: auditLogs } = await findDocumentAuditLogs({ const { data: auditLogs } = await findDocumentAuditLogs({
documentId: documentId, documentId: documentId,
userId: document.userId, userId: document.userId,
teamId: document.teamId || undefined,
perPage: 100_000, perPage: 100_000,
}); });
@@ -103,7 +104,7 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
<span className="font-medium">{_(msg`Owner`)}</span> <span className="font-medium">{_(msg`Owner`)}</span>
<span className="mt-1 block break-words"> <span className="mt-1 block break-words">
{document.User.name} ({document.User.email}) {document.user.name} ({document.user.email})
</span> </span>
</p> </p>
@@ -139,7 +140,7 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
<p className="font-medium">{_(msg`Recipients`)}</p> <p className="font-medium">{_(msg`Recipients`)}</p>
<ul className="mt-1 list-inside list-disc"> <ul className="mt-1 list-inside list-disc">
{document.Recipient.map((recipient) => ( {document.recipients.map((recipient) => (
<li key={recipient.id}> <li key={recipient.id}>
<span className="text-muted-foreground"> <span className="text-muted-foreground">
[{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}] [{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}]

View File

@@ -86,7 +86,7 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
}); });
const isOwner = (email: string) => { const isOwner = (email: string) => {
return email.toLowerCase() === document.User.email.toLowerCase(); return email.toLowerCase() === document.user.email.toLowerCase();
}; };
const getDevice = (userAgent?: string | null) => { const getDevice = (userAgent?: string | null) => {
@@ -104,7 +104,7 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
}; };
const getAuthenticationLevel = (recipientId: number) => { const getAuthenticationLevel = (recipientId: number) => {
const recipient = document.Recipient.find((recipient) => recipient.id === recipientId); const recipient = document.recipients.find((recipient) => recipient.id === recipientId);
if (!recipient) { if (!recipient) {
return 'Unknown'; return 'Unknown';
@@ -157,7 +157,9 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
}; };
const getRecipientSignatureField = (recipientId: number) => { const getRecipientSignatureField = (recipientId: number) => {
return document.Recipient.find((recipient) => recipient.id === recipientId)?.Field.find( return document.recipients
.find((recipient) => recipient.id === recipientId)
?.fields.find(
(field) => field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE, (field) => field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE,
); );
}; };
@@ -181,7 +183,7 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
</TableHeader> </TableHeader>
<TableBody className="print:text-xs"> <TableBody className="print:text-xs">
{document.Recipient.map((recipient, i) => { {document.recipients.map((recipient, i) => {
const logs = getRecipientAuditLogs(recipient.id); const logs = getRecipientAuditLogs(recipient.id);
const signature = getRecipientSignatureField(recipient.id); const signature = getRecipientSignatureField(recipient.id);
@@ -209,17 +211,17 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
boxShadow: `0px 0px 0px 4.88px rgba(122, 196, 85, 0.1), 0px 0px 0px 1.22px rgba(122, 196, 85, 0.6), 0px 0px 0px 0.61px rgba(122, 196, 85, 1)`, boxShadow: `0px 0px 0px 4.88px rgba(122, 196, 85, 0.1), 0px 0px 0px 1.22px rgba(122, 196, 85, 0.6), 0px 0px 0px 0.61px rgba(122, 196, 85, 1)`,
}} }}
> >
{signature.Signature?.signatureImageAsBase64 && ( {signature.signature?.signatureImageAsBase64 && (
<img <img
src={`${signature.Signature?.signatureImageAsBase64}`} src={`${signature.signature?.signatureImageAsBase64}`}
alt="Signature" alt="Signature"
className="max-h-12 max-w-full" className="max-h-12 max-w-full"
/> />
)} )}
{signature.Signature?.typedSignature && ( {signature.signature?.typedSignature && (
<p className="font-signature text-center text-sm"> <p className="font-signature text-center text-sm">
{signature.Signature?.typedSignature} {signature.signature?.typedSignature}
</p> </p>
)} )}
</div> </div>

View File

@@ -7,8 +7,9 @@ import { useSession } from 'next-auth/react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import type { Field, Recipient } from '@documenso/prisma/client'; import type { TTemplate } from '@documenso/lib/types/template';
import type { TemplateWithDetails } from '@documenso/prisma/types/template'; import type { Recipient } from '@documenso/prisma/client';
import type { Field } from '@documenso/prisma/client';
import { import {
DocumentFlowFormContainerActions, DocumentFlowFormContainerActions,
DocumentFlowFormContainerContent, DocumentFlowFormContainerContent,
@@ -40,8 +41,8 @@ export type TConfigureDirectTemplateFormSchema = z.infer<typeof ZConfigureDirect
export type ConfigureDirectTemplateFormProps = { export type ConfigureDirectTemplateFormProps = {
flowStep: DocumentFlowStep; flowStep: DocumentFlowStep;
isDocumentPdfLoaded: boolean; isDocumentPdfLoaded: boolean;
template: Omit<TemplateWithDetails, 'User'>; template: Omit<TTemplate, 'user'>;
directTemplateRecipient: Recipient & { Field: Field[] }; directTemplateRecipient: Recipient & { fields: Field[] };
initialEmail?: string; initialEmail?: string;
onSubmit: (_data: TConfigureDirectTemplateFormSchema) => void; onSubmit: (_data: TConfigureDirectTemplateFormSchema) => void;
}; };
@@ -57,10 +58,10 @@ export const ConfigureDirectTemplateFormPartial = ({
const { _ } = useLingui(); const { _ } = useLingui();
const { data: session } = useSession(); const { data: session } = useSession();
const { Recipient } = template; const { recipients } = template;
const { derivedRecipientAccessAuth } = useRequiredDocumentAuthContext(); const { derivedRecipientAccessAuth } = useRequiredDocumentAuthContext();
const recipientsWithBlankDirectRecipientEmail = Recipient.map((recipient) => { const recipientsWithBlankDirectRecipientEmail = recipients.map((recipient) => {
if (recipient.id === directTemplateRecipient.id) { if (recipient.id === directTemplateRecipient.id) {
return { return {
...recipient, ...recipient,
@@ -74,7 +75,7 @@ export const ConfigureDirectTemplateFormPartial = ({
const form = useForm<TConfigureDirectTemplateFormSchema>({ const form = useForm<TConfigureDirectTemplateFormSchema>({
resolver: zodResolver( resolver: zodResolver(
ZConfigureDirectTemplateFormSchema.superRefine((items, ctx) => { ZConfigureDirectTemplateFormSchema.superRefine((items, ctx) => {
if (template.Recipient.map((recipient) => recipient.email).includes(items.email)) { if (template.recipients.map((recipient) => recipient.email).includes(items.email)) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: _(msg`Email cannot already exist in the template`), message: _(msg`Email cannot already exist in the template`),
@@ -96,7 +97,7 @@ export const ConfigureDirectTemplateFormPartial = ({
<DocumentFlowFormContainerContent> <DocumentFlowFormContainerContent>
{isDocumentPdfLoaded && {isDocumentPdfLoaded &&
directTemplateRecipient.Field.map((field, index) => ( directTemplateRecipient.fields.map((field, index) => (
<ShowFieldItem <ShowFieldItem
key={index} key={index}
field={field} field={field}

View File

@@ -8,9 +8,9 @@ import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
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 { Field } from '@documenso/prisma/client'; import type { Field } from '@documenso/prisma/client';
import { type Recipient } from '@documenso/prisma/client'; import { type Recipient } from '@documenso/prisma/client';
import type { TemplateWithDetails } from '@documenso/prisma/types/template';
import { trpc } from '@documenso/trpc/react'; import { 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';
@@ -28,9 +28,9 @@ import type { DirectTemplateLocalField } from './sign-direct-template';
import { SignDirectTemplateForm } from './sign-direct-template'; import { SignDirectTemplateForm } from './sign-direct-template';
export type TemplatesDirectPageViewProps = { export type TemplatesDirectPageViewProps = {
template: Omit<TemplateWithDetails, 'User'>; template: Omit<TTemplate, 'user'>;
directTemplateToken: string; directTemplateToken: string;
directTemplateRecipient: Recipient & { Field: Field[] }; directTemplateRecipient: Recipient & { fields: Field[] };
}; };
type DirectTemplateStep = 'configure' | 'sign'; type DirectTemplateStep = 'configure' | 'sign';
@@ -164,7 +164,7 @@ export const DirectTemplatePageView = ({
<SignDirectTemplateForm <SignDirectTemplateForm
flowStep={directTemplateFlow.sign} flowStep={directTemplateFlow.sign}
directRecipient={recipient} directRecipient={recipient}
directRecipientFields={directTemplateRecipient.Field} directRecipientFields={directTemplateRecipient.fields}
template={template} template={template}
onSubmit={onSignDirectTemplateSubmit} onSubmit={onSignDirectTemplateSubmit}
/> />

View File

@@ -41,7 +41,7 @@ export default async function TemplatesDirectPage({ params }: TemplatesDirectPag
notFound(); notFound();
} }
const directTemplateRecipient = template.Recipient.find( const directTemplateRecipient = template.recipients.find(
(recipient) => recipient.id === template.directLink?.directTemplateRecipientId, (recipient) => recipient.id === template.directLink?.directTemplateRecipientId,
); );
@@ -81,7 +81,7 @@ export default async function TemplatesDirectPage({ params }: TemplatesDirectPag
<div className="text-muted-foreground mb-8 mt-2.5 flex items-center gap-x-2"> <div className="text-muted-foreground mb-8 mt-2.5 flex items-center gap-x-2">
<UsersIcon className="h-4 w-4" /> <UsersIcon className="h-4 w-4" />
<p className="text-muted-foreground/80"> <p className="text-muted-foreground/80">
<Plural value={template.Recipient.length} one="# recipient" other="# recipients" /> <Plural value={template.recipients.length} one="# recipient" other="# recipients" />
</p> </p>
</div> </div>

View File

@@ -14,10 +14,10 @@ import {
ZRadioFieldMeta, ZRadioFieldMeta,
ZTextFieldMeta, ZTextFieldMeta,
} from '@documenso/lib/types/field-meta'; } from '@documenso/lib/types/field-meta';
import type { TTemplate } from '@documenso/lib/types/template';
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields'; import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { Field, Recipient, Signature } from '@documenso/prisma/client'; import type { Field, Recipient, Signature } from '@documenso/prisma/client';
import { FieldType } from '@documenso/prisma/client'; import { FieldType } from '@documenso/prisma/client';
import type { TemplateWithDetails } from '@documenso/prisma/types/template';
import type { import type {
TRemovedSignedFieldWithTokenMutationSchema, TRemovedSignedFieldWithTokenMutationSchema,
TSignFieldWithTokenMutationSchema, TSignFieldWithTokenMutationSchema,
@@ -55,13 +55,13 @@ export type SignDirectTemplateFormProps = {
flowStep: DocumentFlowStep; flowStep: DocumentFlowStep;
directRecipient: Recipient; directRecipient: Recipient;
directRecipientFields: Field[]; directRecipientFields: Field[];
template: Omit<TemplateWithDetails, 'User'>; template: Omit<TTemplate, 'user'>;
onSubmit: (_data: DirectTemplateLocalField[]) => Promise<void>; onSubmit: (_data: DirectTemplateLocalField[]) => Promise<void>;
}; };
export type DirectTemplateLocalField = Field & { export type DirectTemplateLocalField = Field & {
signedValue?: TSignFieldWithTokenMutationSchema; signedValue?: TSignFieldWithTokenMutationSchema;
Signature?: Signature; signature?: Signature;
}; };
export const SignDirectTemplateForm = ({ export const SignDirectTemplateForm = ({
@@ -95,7 +95,7 @@ export const SignDirectTemplateForm = ({
}; };
if (field.type === FieldType.SIGNATURE) { if (field.type === FieldType.SIGNATURE) {
tempField.Signature = { tempField.signature = {
id: 1, id: 1,
created: new Date(), created: new Date(),
recipientId: 1, recipientId: 1,
@@ -127,7 +127,7 @@ export const SignDirectTemplateForm = ({
customText: '', customText: '',
inserted: false, inserted: false,
signedValue: undefined, signedValue: undefined,
Signature: undefined, signature: undefined,
}; };
}), }),
); );

View File

@@ -52,15 +52,15 @@ export async function GET(_request: Request, { params: { slug } }: SharePageOpen
const isRecipient = 'Signature' in recipientOrSender; const isRecipient = 'Signature' in recipientOrSender;
const signatureImage = match(recipientOrSender) const signatureImage = match(recipientOrSender)
.with({ Signature: P.array(P._) }, (recipient) => { .with({ signatures: P.array(P._) }, (recipient) => {
return recipient.Signature?.[0]?.signatureImageAsBase64 || null; return recipient.signatures?.[0]?.signatureImageAsBase64 || null;
}) })
.otherwise((sender) => { .otherwise((sender) => {
return sender.signature || null; return sender.signature || null;
}); });
const signatureName = match(recipientOrSender) const signatureName = match(recipientOrSender)
.with({ Signature: P.array(P._) }, (recipient) => { .with({ signatures: P.array(P._) }, (recipient) => {
return recipient.name || recipient.email; return recipient.name || recipient.email;
}) })
.otherwise((sender) => { .otherwise((sender) => {

View File

@@ -81,12 +81,12 @@ export const CheckboxField = ({
); );
}, [checkedValues, validationSign, checkboxValidationLength]); }, [checkedValues, validationSign, checkboxValidationLength]);
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } = const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const { const {
mutateAsync: removeSignedFieldWithToken, mutateAsync: removeSignedFieldWithToken,
isLoading: isRemoveSignedFieldWithTokenLoading, isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending; const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;

View File

@@ -51,12 +51,12 @@ export const DateField = ({
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } = const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const { const {
mutateAsync: removeSignedFieldWithToken, mutateAsync: removeSignedFieldWithToken,
isLoading: isRemoveSignedFieldWithTokenLoading, isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending; const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;

View File

@@ -106,7 +106,7 @@ export const DocumentAuthProvider = ({
perPage: MAXIMUM_PASSKEYS, perPage: MAXIMUM_PASSKEYS,
}, },
{ {
keepPreviousData: true, placeholderData: (previousData) => previousData,
enabled: derivedRecipientActionAuth === DocumentAuth.PASSKEY, enabled: derivedRecipientActionAuth === DocumentAuth.PASSKEY,
}, },
); );

View File

@@ -58,12 +58,12 @@ export const DropdownField = ({
const defaultValue = parsedFieldMeta?.defaultValue; const defaultValue = parsedFieldMeta?.defaultValue;
const [localChoice, setLocalChoice] = useState(parsedFieldMeta.defaultValue ?? ''); const [localChoice, setLocalChoice] = useState(parsedFieldMeta.defaultValue ?? '');
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } = const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const { const {
mutateAsync: removeSignedFieldWithToken, mutateAsync: removeSignedFieldWithToken,
isLoading: isRemoveSignedFieldWithTokenLoading, isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending; const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;

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