Compare commits

...

54 Commits

Author SHA1 Message Date
22665543c0 fix: refactor api routes 2024-12-30 21:01:03 +11:00
df33fbf91b feat: admin ui for disabling users (#1547) 2024-12-30 14:45:33 +11:00
ee6efc4cca fix: avoid having a drawn and typed signature at the same time (#1516) 2024-12-27 20:29:12 +11:00
6da15ab12b feat: open the advanced settings automatically (#1508) 2024-12-27 19:57:24 +11:00
Tom
7ef2a8769b chore: update French translations (#1555) 2024-12-27 19:39:57 +11:00
487f52e194 feat: enable optional fields (#1470) 2024-12-27 19:30:44 +11:00
39b1c5bbec feat: additional valid password (#1456) 2024-12-27 16:02:45 +11:00
32857bbfeb fix: make small fields draggable (#1551) 2024-12-27 15:22:56 +11:00
41218e2585 chore: extract translations 2024-12-26 22:08:52 +11:00
a1a2d0801b feat: notify owner when a recipient signs (#1549) 2024-12-26 22:04:13 +11:00
c588c09b26 fix: remove unwanted semicolon (#1545) 2024-12-26 17:28:22 +11:00
74382e21e7 feat: add get recipient route (#1553) 2024-12-26 17:25:14 +11:00
8a7ec7e982 fix: billing page formatting (#1554) 2024-12-26 17:20:08 +11:00
2948a33bf9 fix: tests (#1556) 2024-12-26 17:00:55 +11:00
98b2da5018 v1.9.0-rc.5 2024-12-26 13:41:04 +11:00
fc1f76b543 fix: checkbox logic (#1537)
## Description

<!--- Describe the changes introduced by this pull request. -->
<!--- Explain what problem it solves or what feature/fix it adds. -->

## Related Issue

<!--- If this pull request is related to a specific issue, reference it
here using #issue_number. -->
<!--- For example, "Fixes #123" or "Addresses #456". -->

## Changes Made

<!--- Provide a summary of the changes made in this pull request. -->
<!--- Include any relevant technical details or architecture changes.
-->

- Change 1
- Change 2
- ...

## Testing Performed

<!--- Describe the testing that you have performed to validate these
changes. -->
<!--- Include information about test cases, testing environments, and
results. -->

- Tested feature X in scenario Y.
- Ran unit tests for component Z.
- Tested on browsers A, B, and C.
- ...

## Checklist

<!--- Please check the boxes that apply to this pull request. -->
<!--- You can add or remove items as needed. -->

- [ ] I have tested these changes locally and they work as expected.
- [ ] I have added/updated tests that prove the effectiveness of these
changes.
- [ ] I have updated the documentation to reflect these changes, if
applicable.
- [ ] I have followed the project's coding style guidelines.
- [ ] I have addressed the code review feedback from the previous
submission, if applicable.

## Additional Notes

<!--- Provide any additional context or notes for the reviewers. -->
<!--- This might include details about design decisions, potential
concerns, or anything else relevant. -->
2024-12-23 12:06:47 +02:00
22c9fb777b fix: perf improvements 2024-12-18 15:01:57 +11:00
2da051a7f9 v1.9.0-rc.4 2024-12-18 08:14:50 +11:00
390a317bd3 fix: normalize pdf on the server 2024-12-18 08:14:14 +11:00
c161553d1d feat: add disabled property for user (#1546) 2024-12-17 22:35:59 +11:00
c960a48b4f fix: z-index of field settings (#1469) 2024-12-17 17:09:58 +11:00
9502f4361d fix: fieldtooltip blocking the field click (#1538)
## Before (error)


https://github.com/user-attachments/assets/525e6c04-fc03-41a7-8299-2a753e9e9fa6

## After (fixed)


https://github.com/user-attachments/assets/67f7e592-c5ca-47f4-962c-e4a848522d43
2024-12-17 17:04:20 +11:00
82deab41f4 fix: move permission check outside the document visibility component (#1543)
PR created because of this comment
https://github.com/documenso/documenso/pull/1521#discussion_r1881895305.
2024-12-17 17:03:08 +11:00
2245812f0b fix: document visibility logic (#1521)
Update the logic of document visibility logic and added some tests &
updated some existing ones.
2024-12-16 09:10:40 +02:00
861e9c976b v1.9.0-rc.3 2024-12-16 09:35:33 +11:00
f55808199b feat: make enterprise billing dynamic (#1539) 2024-12-14 13:44:25 +09:00
b4a7f1887d feat: add trpc openapi (#1535) 2024-12-14 01:23:35 +09:00
f73441ee85 chore: prevent user selection within signature pad (#1530)
adds a `select-none` class to the signature pad in order to
prevent iPadOS from becoming too trigger happy with the context menu and
auto corrects. Please ensure this doesn't break anything by accident.
2024-12-13 16:02:26 +11:00
d7de3b08c1 fix: darkmode on radio button and checkbox labels (#1518)
Fixed Radio Button and Checkbox Appearance in Dark Mode
2024-12-13 15:55:40 +11:00
7d201f05d9 fix: admin leaderboard query (#1522)
Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
2024-12-13 15:50:52 +11:00
a21ee2cea6 v1.9.0-rc.2 2024-12-13 15:16:26 +11:00
4ad46b81c9 fix: prevent hidden layers from being toggled in pdf viewers (#1528)
https://github.com/user-attachments/assets/e10194ca-212b-40ee-b9a1-85ef54829a40

---------

Co-authored-by: Mythie <me@lucasjamessmith.me>
2024-12-13 14:19:55 +11:00
10b8e785e0 fix: clear invalid drawn signature when switching to typed signature (#1536) 2024-12-13 10:40:22 +11:00
5fbed783fc feat: add controls for sending completion emails to document owners (#1534)
Adds a new `ownerDocumentCompleted` to the email settings that controls
whether a document will be sent to the owner upon completion.

This was previously the only email you couldn't disable and didn't
account for users integrating with just the API and Webhooks.

Also adds a flag to the public `sendDocument` endpoint which will adjust
this setting while sendint the document for users who aren't using
`emailSettings` on the `createDocument` endpoint.
2024-12-12 14:24:07 +11:00
c9fe134852 v1.9.0-rc.1 2024-12-12 10:31:44 +11:00
f2149719e3 fix: resolve issue with embed css injection 2024-12-12 10:14:23 +11:00
161d40cde7 fix: secure passkey cookies (#1533) 2024-12-12 01:16:29 +09:00
76028771b8 fix: add billing leeway (#1532) 2024-12-12 01:10:01 +09:00
5df1a6602e fix: refactor search routes (#1529)
Refactor find endpoints to be consistent in terms of input and output.
2024-12-11 19:39:50 +09:00
3d7b28a92b chore: update tailwind config 2024-12-11 13:52:34 +11:00
ed862413b1 v1.9.0-rc.0 2024-12-11 09:48:01 +11:00
9d02ab4a5e feat: open page api (#1419) 2024-12-10 21:19:05 +11:00
34c0868d77 chore: add openapi description for enterprise field (#1520) 2024-12-10 20:26:28 +11:00
fae9c0ca24 fix: refactor routers (#1523) 2024-12-10 16:11:20 +09:00
dd162205fa fix: prevent accidental signatures (#1515)
![CleanShot 2024-12-06 at 03 30
39](https://github.com/user-attachments/assets/d47dc820-f19d-43b7-a60d-914fc9ab24b8)

![CleanShot 2024-12-06 at 03 32
34](https://github.com/user-attachments/assets/0db98735-8c91-469b-873c-adb19d0fff7b)
2024-12-08 14:17:58 +11:00
a88ae1cc1e chore: extract translations 2024-12-06 16:11:54 +09:00
904948e2bc fix: refactor trpc errors (#1511) 2024-12-06 16:01:24 +09:00
3b6b96f551 chore: remove redundant translations on upload (#1510)
## Description

Clean redundant translations by default.

This should stop the AI from doing strange things to commented out
translations.
2024-12-06 09:04:15 +11:00
67e49c82a3 feat: return fields in GET /documents/:id endpoint (#1317)
To be able to use the PATCH `/api/v1/documents/{id}/fields/{fieldId}`
endpoint, we need to know the fields ID of a particular document. The
issue #1178 suggested to create a new endpoint for this. To be
consistent with the `/templates` endpoint, I propose in this PR to
directly add the `fields` field to the `/documents/:id` endpoint.
2024-12-06 09:03:32 +11:00
9f45fe62e4 fix: refactor teams router (#1500) 2024-12-05 22:14:47 +09:00
9e8094e34c Update README.md (#1509)
🚨 WE ARE LIVE ON PH WITH OUR LATEST LAUNCH 🚀
2024-12-05 13:51:49 +01:00
0e7e9e17c9 v1.8.1 2024-12-05 13:57:05 +11:00
b3ccb3d26f v1.8.1-rc.9 2024-12-05 13:53:35 +11:00
b17370c153 chore: reword some german translations to increase clarity (#1507) 2024-12-05 09:42:10 +11:00
347 changed files with 10168 additions and 7004 deletions

View File

@ -10,13 +10,7 @@
"ghcr.io/devcontainers/features/node:1": {}
},
"onCreateCommand": "./.devcontainer/on-create.sh",
"forwardPorts": [
3000,
54320,
9000,
2500,
1100
],
"forwardPorts": [3000, 54320, 9000, 2500, 1100],
"customizations": {
"vscode": {
"extensions": [
@ -35,4 +29,4 @@
]
}
}
}
}

View File

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

View File

@ -1,3 +1,7 @@
> 🚨 We are live on Product Hunt 🎉 Check out our latest launch: <a href="documen.so/sign-everywhere">The Platform Plan</a>!
<a href="https://www.producthunt.com/posts/documenso-platform-plan?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-documenso&#0045;platform&#0045;plan" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=670576&theme=light" alt="Documenso&#0032;Platform&#0032;Plan - Whitelabeled&#0032;signing&#0032;flows&#0032;in&#0032;your&#0032;product | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<img src="https://github.com/documenso/documenso/assets/13398220/a643571f-0239-46a6-a73e-6bef38d1228b" alt="Documenso Logo">
<p align="center" style="margin-top: 20px">

View File

@ -1,36 +1 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3002](http://localhost:3002) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
# @documenso/documentation

View File

@ -4,7 +4,7 @@ import '../styles.css';
export default function App({ Component, pageProps }) {
return (
<PlausibleProvider>
<Component {...pageProps} />;
<Component {...pageProps} />
</PlausibleProvider>
);
}

View File

@ -17,23 +17,25 @@ The default document visibility option allows you to control who can view and ac
The default document visibility is set to "_EVERYONE_" by default. You can change this setting by going to the [team's general preferences page](/users/teams/preferences) and selecting a different visibility option.
<Callout type="warning">
If the team member uploading the document has a role lower than the default document visibility,
the document visibility will be set to a lower visibility level matching the team member's role.
</Callout>
Here's how it works:
- If a user with the "_Member_" role creates a document and the default document visibility is set to "_Admin_" or "_Managers and above_", the document's visibility is set to "_Everyone_".
- If a user with the "_Manager_" role creates a document and the default document visibility is set to "_Admin_", the document's visibility is set to "_Managers and above_".
- Otherwise, the document's visibility is set to the default document visibility.
- If a user with the "_Member_" role creates a document and the default document visibility is set to "_Everyone_", the document's visibility is set to "_EVERYONE_".
- The user can't change the visibility of the document in the document editor.
- If a user with the "_Member_" role creates a document and the default document visibility is set to "_Admin_" or "_Managers and above_", the document's visibility is set to the default document visibility ("_Admin_" or "_Managers and above_" in this case).
- The user can't change the visibility of the document in the document editor.
- If a user with the "_Manager_" role creates a document and the default document visibility is set to "_Everyone_" or "_Managers and above_", the document's visibility is set to the default document visibility ("_Everyone_" or "_Managers and above_" in this case).
- The user can change the visibility of the document to any of these options, except "_Admin_", in the document editor.
- If a user with the "_Manager_" role creates a document and the default document visibility is set to "_Admin_", the document's visibility is set to "_Admin_".
- The user can't change the visibility of the document in the document editor.
- If a user with the "_Admin_" role creates a document, and the default document visibility is set to "_Everyone_", "_Managers and above_", or "_Admin_", the document's visibility is set to the default document visibility.
- The user can change the visibility of the document to any of these options in the document editor.
You can change the visibility of a document at any time by editing the document and selecting a different visibility option.
![A screenshot of the Documenso's document editor page where you can update the document visibility](/teams/document-visibility-settings.webp)
<Callout type="warning">
Updating the default document visibility in the team's general settings will not affect the
Updating the default document visibility in the team's general preferences will not affect the
visibility of existing documents. You will need to update the visibility of each document
individually.
</Callout>

View File

@ -1,6 +1,6 @@
{
"name": "@documenso/marketing",
"version": "1.8.1-rc.8",
"version": "1.9.0-rc.5",
"private": true,
"license": "AGPL-3.0",
"scripts": {
@ -47,7 +47,7 @@
"recharts": "^2.7.2",
"sharp": "0.32.6",
"typescript": "5.2.2",
"zod": "^3.22.4"
"zod": "3.24.1"
},
"devDependencies": {
"@lingui/loader": "^4.11.3",

View File

@ -10,16 +10,13 @@ import { msg } from '@lingui/macro';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { base64 } from '@documenso/lib/universal/base64';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import type { Field, Recipient } from '@documenso/prisma/client';
import { DocumentDataType, Prisma } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
import { AddFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add-fields';
import type { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types';
import { AddSignatureFormPartial } from '@documenso/ui/primitives/document-flow/add-signature';
import type { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types';
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
@ -43,9 +40,6 @@ export const SinglePlayerClient = () => {
const [step, setStep] = useState<SinglePlayerModeStep>('fields');
const [fields, setFields] = useState<Field[]>([]);
const { mutateAsync: createSinglePlayerDocument } =
trpc.singleplayer.createSinglePlayerDocument.useMutation();
const documentFlow: Record<SinglePlayerModeStep, DocumentFlowStep> = {
fields: {
title: msg`Add document`,
@ -112,38 +106,35 @@ export const SinglePlayerClient = () => {
/**
* Upload, create, sign and send the document.
*/
const onSignSubmit = async (data: TAddSignatureFormSchema) => {
const onSignSubmit = (data: unknown) => {
if (!uploadedFile) {
return;
}
try {
const putFileData = await putPdfFile(uploadedFile.file);
const documentToken = await createSinglePlayerDocument({
documentData: {
type: putFileData.type,
data: putFileData.data,
},
documentName: uploadedFile.file.name,
signer: data,
fields: fields.map((field) => ({
page: field.page,
type: field.type,
positionX: field.positionX.toNumber(),
positionY: field.positionY.toNumber(),
width: field.width.toNumber(),
height: field.height.toNumber(),
fieldMeta: field.fieldMeta,
})),
fieldMeta: { type: undefined },
});
analytics.capture('Marketing: SPM - Document signed', {
signer: data.email,
});
router.push(`/singleplayer/${documentToken}/success`);
// const putFileData = await putPdfFile(uploadedFile.file);
// const documentToken = await createSinglePlayerDocument({
// documentData: {
// type: putFileData.type,
// data: putFileData.data,
// },
// documentName: uploadedFile.file.name,
// signer: data,
// fields: fields.map((field) => ({
// page: field.page,
// type: field.type,
// positionX: field.positionX.toNumber(),
// positionY: field.positionY.toNumber(),
// width: field.width.toNumber(),
// height: field.height.toNumber(),
// fieldMeta: field.fieldMeta,
// })),
// fieldMeta: { type: undefined },
// });
// analytics.capture('Marketing: SPM - Document signed', {
// signer: data.email,
// });
// router.push(`/singleplayer/${documentToken}/success`);
} catch {
toast({
title: 'Something went wrong',

40
apps/openpage-api/.gitignore vendored Normal file
View File

@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# env files (can opt-in for commiting if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@ -0,0 +1 @@
# @documenso/openpage-api

View File

@ -0,0 +1,36 @@
import type { NextRequest } from 'next/server';
import cors from '@/lib/cors';
const paths = [
{ path: '/total-prs', description: 'Total GitHub Merged PRs' },
{ path: '/total-stars', description: 'Total GitHub Stars' },
{ path: '/total-forks', description: 'Total GitHub Forks' },
{ path: '/total-issues', description: 'Total GitHub Issues' },
];
export function GET(request: NextRequest) {
const url = request.nextUrl.toString();
const apis = paths.map(({ path, description }) => {
return { path: url + path, description };
});
return cors(
request,
new Response(JSON.stringify(apis), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@ -0,0 +1,27 @@
import cors from '@/lib/cors';
import { transformData } from '@/lib/transform-data';
export async function GET(request: Request) {
const res = await fetch('https://stargrazer-live.onrender.com/api/stats');
const data = await res.json();
const transformedData = transformData({ data, metric: 'forks' });
return cors(
request,
new Response(JSON.stringify(transformedData), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@ -0,0 +1,27 @@
import cors from '@/lib/cors';
import { transformData } from '@/lib/transform-data';
export async function GET(request: Request) {
const res = await fetch('https://stargrazer-live.onrender.com/api/stats');
const data = await res.json();
const transformedData = transformData({ data, metric: 'openIssues' });
return cors(
request,
new Response(JSON.stringify(transformedData), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@ -0,0 +1,27 @@
import cors from '@/lib/cors';
import { transformData } from '@/lib/transform-data';
export async function GET(request: Request) {
const res = await fetch('https://stargrazer-live.onrender.com/api/stats');
const data = await res.json();
const transformedData = transformData({ data, metric: 'mergedPRs' });
return cors(
request,
new Response(JSON.stringify(transformedData), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@ -0,0 +1,27 @@
import cors from '@/lib/cors';
import { transformData } from '@/lib/transform-data';
export async function GET(request: Request) {
const res = await fetch('https://stargrazer-live.onrender.com/api/stats');
const data = await res.json();
const transformedData = transformData({ data, metric: 'stars' });
return cors(
request,
new Response(JSON.stringify(transformedData), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@ -0,0 +1,25 @@
import cors from '@/lib/cors';
export async function GET(request: Request) {
const res = await fetch('https://api.github.com/repos/documenso/documenso');
const { forks_count } = await res.json();
return cors(
request,
new Response(JSON.stringify({ data: forks_count }), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@ -0,0 +1,27 @@
import cors from '@/lib/cors';
export async function GET(request: Request) {
const res = await fetch(
'https://api.github.com/search/issues?q=repo:documenso/documenso+type:issue+state:open&page=0&per_page=1',
);
const { total_count } = await res.json();
return cors(
request,
new Response(JSON.stringify({ data: total_count }), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@ -0,0 +1,27 @@
import cors from '@/lib/cors';
export async function GET(request: Request) {
const res = await fetch(
'https://api.github.com/search/issues?q=repo:documenso/documenso/+is:pr+merged:>=2010-01-01&page=0&per_page=1',
);
const { total_count } = await res.json();
return cors(
request,
new Response(JSON.stringify({ data: total_count }), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@ -0,0 +1,36 @@
import type { NextRequest } from 'next/server';
import cors from '@/lib/cors';
const paths = [
{ path: '/forks', description: 'GitHub Forks' },
{ path: '/stars', description: 'GitHub Stars' },
{ path: '/issues', description: 'GitHub Merged Issues' },
{ path: '/prs', description: 'GitHub Pull Requests' },
];
export function GET(request: NextRequest) {
const url = request.nextUrl.toString();
const apis = paths.map(({ path, description }) => {
return { path: url + path, description };
});
return cors(
request,
new Response(JSON.stringify(apis), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@ -0,0 +1,25 @@
import cors from '@/lib/cors';
export async function GET(request: Request) {
const res = await fetch('https://api.github.com/repos/documenso/documenso');
const { stargazers_count } = await res.json();
return cors(
request,
new Response(JSON.stringify({ data: stargazers_count }), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@ -0,0 +1,25 @@
import cors from '@/lib/cors';
import { getCompletedDocumentsMonthly } from '@/lib/growth/get-monthly-completed-document';
export async function GET(request: Request) {
const completedDocuments = await getCompletedDocumentsMonthly();
return cors(
request,
new Response(JSON.stringify(completedDocuments), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@ -0,0 +1,25 @@
import cors from '@/lib/cors';
import { getUserMonthlyGrowth } from '@/lib/growth/get-user-monthly-growth';
export async function GET(request: Request) {
const monthlyUsers = await getUserMonthlyGrowth();
return cors(
request,
new Response(JSON.stringify(monthlyUsers), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@ -0,0 +1,39 @@
import type { NextRequest } from 'next/server';
import cors from '@/lib/cors';
const paths = [
{ path: '/total-customers', description: 'Total Customers' },
{ path: '/total-users', description: 'Total Users' },
{ path: '/new-users', description: 'New Users' },
{ path: '/completed-documents', description: 'Completed Documents per Month' },
{ path: '/total-completed-documents', description: 'Total Completed Documents' },
{ path: '/signer-conversion', description: 'Signers That Signed Up' },
{ path: '/total-signer-conversion', description: 'Total Signers That Signed Up' },
];
export function GET(request: NextRequest) {
const url = request.nextUrl.toString();
const apis = paths.map(({ path, description }) => {
return { path: url + path, description };
});
return cors(
request,
new Response(JSON.stringify(apis), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@ -0,0 +1,25 @@
import cors from '@/lib/cors';
import { getSignerConversionMonthly } from '@/lib/growth/get-signer-conversion';
export async function GET(request: Request) {
const signers = await getSignerConversionMonthly();
return cors(
request,
new Response(JSON.stringify(signers), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@ -0,0 +1,25 @@
import cors from '@/lib/cors';
import { getCompletedDocumentsMonthly } from '@/lib/growth/get-monthly-completed-document';
export async function GET(request: Request) {
const totalCompletedDocuments = await getCompletedDocumentsMonthly('cumulative');
return cors(
request,
new Response(JSON.stringify(totalCompletedDocuments), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@ -0,0 +1,31 @@
import cors from '@/lib/cors';
import { transformData } from '@/lib/transform-data';
export async function GET(request: Request) {
const res = await fetch('https://stargrazer-live.onrender.com/api/stats/stripe');
const EARLY_ADOPTERS_DATA = await res.json();
const transformedData = transformData({
data: EARLY_ADOPTERS_DATA,
metric: 'earlyAdopters',
});
return cors(
request,
new Response(JSON.stringify(transformedData), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@ -0,0 +1,25 @@
import cors from '@/lib/cors';
import { getSignerConversionMonthly } from '@/lib/growth/get-signer-conversion';
export async function GET(request: Request) {
const totalSigners = await getSignerConversionMonthly('cumulative');
return cors(
request,
new Response(JSON.stringify(totalSigners), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@ -0,0 +1,25 @@
import cors from '@/lib/cors';
import { getUserMonthlyGrowth } from '@/lib/growth/get-user-monthly-growth';
export async function GET(request: Request) {
const totalUsers = await getUserMonthlyGrowth('cumulative');
return cors(
request,
new Response(JSON.stringify(totalUsers), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@ -0,0 +1,35 @@
import type { NextRequest } from 'next/server';
import cors from '@/lib/cors';
const paths = [
{ path: 'github', description: 'GitHub Data' },
{ path: 'community', description: 'Community Data' },
{ path: 'growth', description: 'Growth Data' },
];
export function GET(request: NextRequest) {
const url = request.nextUrl.toString();
const apis = paths.map(({ path, description }) => {
return { path: url + path, description };
});
return cors(
request,
new Response(JSON.stringify(apis), {
status: 200,
headers: {
'content-type': 'application/json',
},
}),
);
}
export function OPTIONS(request: Request) {
return cors(
request,
new Response(null, {
status: 204,
}),
);
}

View File

@ -0,0 +1,135 @@
/**
* Multi purpose CORS lib.
* Note: Based on the `cors` package in npm but using only web APIs.
* Taken from: https://github.com/vercel/examples/blob/main/edge-functions/cors/lib/cors.ts
*/
type StaticOrigin = boolean | string | RegExp | (boolean | string | RegExp)[];
type OriginFn = (origin: string | undefined, req: Request) => StaticOrigin | Promise<StaticOrigin>;
interface CorsOptions {
origin?: StaticOrigin | OriginFn;
methods?: string | string[];
allowedHeaders?: string | string[];
exposedHeaders?: string | string[];
credentials?: boolean;
maxAge?: number;
preflightContinue?: boolean;
optionsSuccessStatus?: number;
}
const defaultOptions: CorsOptions = {
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
preflightContinue: false,
optionsSuccessStatus: 204,
};
function isOriginAllowed(origin: string, allowed: StaticOrigin): boolean {
return Array.isArray(allowed)
? allowed.some((o) => isOriginAllowed(origin, o))
: typeof allowed === 'string'
? origin === allowed
: allowed instanceof RegExp
? allowed.test(origin)
: !!allowed;
}
function getOriginHeaders(reqOrigin: string | undefined, origin: StaticOrigin) {
const headers = new Headers();
if (origin === '*') {
headers.set('Access-Control-Allow-Origin', '*');
} else if (typeof origin === 'string') {
headers.set('Access-Control-Allow-Origin', origin);
headers.append('Vary', 'Origin');
} else {
const allowed = isOriginAllowed(reqOrigin ?? '', origin);
if (allowed && reqOrigin) {
headers.set('Access-Control-Allow-Origin', reqOrigin);
}
headers.append('Vary', 'Origin');
}
return headers;
}
async function originHeadersFromReq(req: Request, origin: StaticOrigin | OriginFn) {
const reqOrigin = req.headers.get('Origin') || undefined;
const value = typeof origin === 'function' ? await origin(reqOrigin, req) : origin;
if (!value) return;
return getOriginHeaders(reqOrigin, value);
}
function getAllowedHeaders(req: Request, allowed?: string | string[]) {
const headers = new Headers();
if (!allowed) {
allowed = req.headers.get('Access-Control-Request-Headers')!;
headers.append('Vary', 'Access-Control-Request-Headers');
} else if (Array.isArray(allowed)) {
allowed = allowed.join(',');
}
if (allowed) {
headers.set('Access-Control-Allow-Headers', allowed);
}
return headers;
}
export default async function cors(req: Request, res: Response, options?: CorsOptions) {
const opts = { ...defaultOptions, ...options };
const { headers } = res;
const originHeaders = await originHeadersFromReq(req, opts.origin ?? false);
const mergeHeaders = (v: string, k: string) => {
if (k === 'Vary') headers.append(k, v);
else headers.set(k, v);
};
// If there's no origin we won't touch the response
if (!originHeaders) return res;
originHeaders.forEach(mergeHeaders);
if (opts.credentials) {
headers.set('Access-Control-Allow-Credentials', 'true');
}
const exposed = Array.isArray(opts.exposedHeaders)
? opts.exposedHeaders.join(',')
: opts.exposedHeaders;
if (exposed) {
headers.set('Access-Control-Expose-Headers', exposed);
}
// Handle the preflight request
if (req.method === 'OPTIONS') {
if (opts.methods) {
const methods = Array.isArray(opts.methods) ? opts.methods.join(',') : opts.methods;
headers.set('Access-Control-Allow-Methods', methods);
}
getAllowedHeaders(req, opts.allowedHeaders).forEach(mergeHeaders);
if (typeof opts.maxAge === 'number') {
headers.set('Access-Control-Max-Age', String(opts.maxAge));
}
if (opts.preflightContinue) return res;
headers.set('Content-Length', '0');
return new Response(null, { status: opts.optionsSuccessStatus, headers });
}
// If we got here, it's a normal request
return res;
}
export function initCors(options?: CorsOptions) {
return async (req: Request, res: Response) => cors(req, res, options);
}

View File

@ -0,0 +1,43 @@
import { DateTime } from 'luxon';
import { kyselyPrisma, sql } from '@documenso/prisma';
import { DocumentStatus } from '@documenso/prisma/client';
export const getCompletedDocumentsMonthly = async (type: 'count' | 'cumulative' = 'count') => {
const qb = kyselyPrisma.$kysely
.selectFrom('Document')
.select(({ fn }) => [
fn<Date>('DATE_TRUNC', [sql.lit('MONTH'), 'Document.updatedAt']).as('month'),
fn.count('id').as('count'),
fn
.sum(fn.count('id'))
// Feels like a bug in the Kysely extension but I just can not do this orderBy in a type-safe manner
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
.over((ob) => ob.orderBy(fn('DATE_TRUNC', [sql.lit('MONTH'), 'Document.updatedAt']) as any))
.as('cume_count'),
])
.where(() => sql`"Document"."status" = ${DocumentStatus.COMPLETED}::"DocumentStatus"`)
.groupBy('month')
.orderBy('month', 'desc')
.limit(12);
const result = await qb.execute();
const transformedData = {
labels: result.map((row) => DateTime.fromJSDate(row.month).toFormat('MMM yyyy')).reverse(),
datasets: [
{
label: type === 'count' ? 'Completed Documents per Month' : 'Total Completed Documents',
data: result
.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count)))
.reverse(),
},
],
};
return transformedData;
};
export type GetCompletedDocumentsMonthlyResult = Awaited<
ReturnType<typeof getCompletedDocumentsMonthly>
>;

View File

@ -0,0 +1,42 @@
import { DateTime } from 'luxon';
import { kyselyPrisma, sql } from '@documenso/prisma';
export const getSignerConversionMonthly = async (type: 'count' | 'cumulative' = 'count') => {
const qb = kyselyPrisma.$kysely
.selectFrom('Recipient')
.innerJoin('User', 'Recipient.email', 'User.email')
.select(({ fn }) => [
fn<Date>('DATE_TRUNC', [sql.lit('MONTH'), 'User.createdAt']).as('month'),
fn.count('Recipient.email').distinct().as('count'),
fn
.sum(fn.count('Recipient.email').distinct())
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
.over((ob) => ob.orderBy(fn('DATE_TRUNC', [sql.lit('MONTH'), 'User.createdAt']) as any))
.as('cume_count'),
])
.where('Recipient.signedAt', 'is not', null)
.where('Recipient.signedAt', '<', (eb) => eb.ref('User.createdAt'))
.groupBy(({ fn }) => fn('DATE_TRUNC', [sql.lit('MONTH'), 'User.createdAt']))
.orderBy('month', 'desc');
const result = await qb.execute();
const transformedData = {
labels: result.map((row) => DateTime.fromJSDate(row.month).toFormat('MMM yyyy')).reverse(),
datasets: [
{
label: type === 'count' ? 'Signers That Signed Up' : 'Total Signers That Signed Up',
data: result
.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count)))
.reverse(),
},
],
};
return transformedData;
};
export type GetSignerConversionMonthlyResult = Awaited<
ReturnType<typeof getSignerConversionMonthly>
>;

View File

@ -0,0 +1,38 @@
import { DateTime } from 'luxon';
import { kyselyPrisma, sql } from '@documenso/prisma';
export const getUserMonthlyGrowth = async (type: 'count' | 'cumulative' = 'count') => {
const qb = kyselyPrisma.$kysely
.selectFrom('User')
.select(({ fn }) => [
fn<Date>('DATE_TRUNC', [sql.lit('MONTH'), 'User.createdAt']).as('month'),
fn.count('id').as('count'),
fn
.sum(fn.count('id'))
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
.over((ob) => ob.orderBy(fn('DATE_TRUNC', [sql.lit('MONTH'), 'User.createdAt']) as any))
.as('cume_count'),
])
.groupBy('month')
.orderBy('month', 'desc')
.limit(12);
const result = await qb.execute();
const transformedData = {
labels: result.map((row) => DateTime.fromJSDate(row.month).toFormat('MMM yyyy')).reverse(),
datasets: [
{
label: type === 'count' ? 'New Users' : 'Total Users',
data: result
.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count)))
.reverse(),
},
],
};
return transformedData;
};
export type GetUserMonthlyGrowthResult = Awaited<ReturnType<typeof getUserMonthlyGrowth>>;

View File

@ -0,0 +1,68 @@
import { DateTime } from 'luxon';
type MetricKeys = {
stars: number;
forks: number;
mergedPRs: number;
openIssues: number;
earlyAdopters: number;
};
type DataEntry = {
[key: string]: MetricKeys;
};
type TransformData = {
labels: string[];
datasets: {
label: string;
data: number[];
}[];
};
type MetricKey = keyof MetricKeys;
const FRIENDLY_METRIC_NAMES: { [key in MetricKey]: string } = {
stars: 'Stars',
forks: 'Forks',
mergedPRs: 'Merged PRs',
openIssues: 'Open Issues',
earlyAdopters: 'Customers',
};
export function transformData({
data,
metric,
}: {
data: DataEntry;
metric: MetricKey;
}): TransformData {
const sortedEntries = Object.entries(data).sort(([dateA], [dateB]) => {
const [yearA, monthA] = dateA.split('-').map(Number);
const [yearB, monthB] = dateB.split('-').map(Number);
return DateTime.local(yearA, monthA).toMillis() - DateTime.local(yearB, monthB).toMillis();
});
const labels = sortedEntries.map(([date]) => {
const [year, month] = date.split('-');
const dateTime = DateTime.fromObject({
year: Number(year),
month: Number(month),
});
return dateTime.toFormat('MMM yyyy');
});
return {
labels,
datasets: [
{
label: `Total ${FRIENDLY_METRIC_NAMES[metric]}`,
data: sortedEntries.map(([_, stats]) => stats[metric]),
},
],
};
}
// To be on the safer side
export const transformRepoStats = transformData;

View File

@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
module.exports = nextConfig;

View File

@ -0,0 +1,23 @@
{
"name": "@documenso/openpage-api",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev -p 3003",
"build": "next build",
"start": "next start",
"lint:fix": "next lint --fix",
"clean": "rimraf .next && rimraf node_modules",
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
},
"dependencies": {
"@documenso/prisma": "*",
"luxon": "^3.5.0",
"next": "14.2.6"
},
"devDependencies": {
"@types/node": "20.16.5",
"@types/react": "18.3.5",
"typescript": "5.5.4"
}
}

View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@ -1,6 +1,6 @@
{
"name": "@documenso/web",
"version": "1.8.1-rc.8",
"version": "1.9.0-rc.5",
"private": true,
"license": "AGPL-3.0",
"scripts": {
@ -56,10 +56,11 @@
"recharts": "^2.7.2",
"remeda": "^2.17.3",
"sharp": "0.32.6",
"trpc-openapi": "^1.2.0",
"ts-pattern": "^5.0.5",
"ua-parser-js": "^1.0.37",
"uqr": "^0.1.2",
"zod": "^3.22.4"
"zod": "3.24.1"
},
"devDependencies": {
"@documenso/tailwind-config": "*",

View File

@ -40,7 +40,7 @@ export const AdminDocumentResults = () => {
const { data: findDocumentsData, isLoading: isFindDocumentsLoading } =
trpc.admin.findDocuments.useQuery(
{
term: debouncedTerm,
query: debouncedTerm,
page: page || 1,
perPage: perPage || 20,
},

View File

@ -134,7 +134,7 @@ export const LeaderboardTable = ({
startTransition(() => {
updateSearchParams({
sortBy: column,
sortOrder: sortOrder === 'asc' ? 'desc' : 'asc',
sortOrder: sortBy === column && sortOrder === 'asc' ? 'desc' : 'asc',
});
});
};

View File

@ -30,8 +30,8 @@ export type DeleteUserDialogProps = {
};
export const DeleteUserDialog = ({ className, user }: DeleteUserDialogProps) => {
const { toast } = useToast();
const { _ } = useLingui();
const { toast } = useToast();
const router = useRouter();
@ -44,7 +44,6 @@ export const DeleteUserDialog = ({ className, user }: DeleteUserDialogProps) =>
try {
await deleteUser({
id: user.id,
email,
});
toast({
@ -78,7 +77,7 @@ export const DeleteUserDialog = ({ className, user }: DeleteUserDialogProps) =>
return (
<div className={className}>
<Alert
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row "
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<div>

View File

@ -0,0 +1,141 @@
'use client';
import { useState } from 'react';
import { Trans, msg } from '@lingui/macro';
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 { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type DisableUserDialogProps = {
className?: string;
userToDisable: User;
};
export const DisableUserDialog = ({ className, userToDisable }: DisableUserDialogProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const [email, setEmail] = useState('');
const { mutateAsync: disableUser, isLoading: isDisablingUser } =
trpc.admin.disableUser.useMutation();
const onDisableAccount = async () => {
try {
await disableUser({
id: userToDisable.id,
});
toast({
title: _(msg`Account disabled`),
description: _(msg`The account has been disabled successfully.`),
duration: 5000,
});
} catch (err) {
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 disable this user.`)
.otherwise(() => msg`An error occurred while disabling the user.`);
toast({
title: _(msg`Error`),
description: _(errorMessage),
variant: 'destructive',
duration: 7500,
});
}
};
return (
<div className={className}>
<Alert
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<div>
<AlertTitle>Disable Account</AlertTitle>
<AlertDescription className="mr-2">
<Trans>
Disabling the user results in the user not being able to use the account. It also
disables all the related contents such as subscription, webhooks, teams, and API keys.
</Trans>
</AlertDescription>
</div>
<div className="flex-shrink-0">
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">
<Trans>Disable Account</Trans>
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader className="space-y-4">
<DialogTitle>
<Trans>Disable Account</Trans>
</DialogTitle>
<Alert variant="destructive">
<AlertDescription className="selection:bg-red-100">
<Trans>
This action is reversible, but please be careful as the account may be
affected permanently (e.g. their settings and contents not being restored
properly).
</Trans>
</AlertDescription>
</Alert>
</DialogHeader>
<div>
<DialogDescription>
<Trans>
To confirm, please enter the accounts email address <br />({userToDisable.email}
).
</Trans>
</DialogDescription>
<Input
className="mt-2"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<DialogFooter>
<Button
onClick={onDisableAccount}
loading={isDisablingUser}
variant="destructive"
disabled={email !== userToDisable.email}
>
<Trans>Disable account</Trans>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</Alert>
</div>
);
};

View File

@ -0,0 +1,130 @@
'use client';
import { useState } from 'react';
import { Trans, msg } from '@lingui/macro';
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 { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type EnableUserDialogProps = {
className?: string;
userToEnable: User;
};
export const EnableUserDialog = ({ className, userToEnable }: EnableUserDialogProps) => {
const { toast } = useToast();
const { _ } = useLingui();
const [email, setEmail] = useState('');
const { mutateAsync: enableUser, isLoading: isEnablingUser } =
trpc.admin.enableUser.useMutation();
const onEnableAccount = async () => {
try {
await enableUser({
id: userToEnable.id,
});
toast({
title: _(msg`Account enabled`),
description: _(msg`The account has been enabled successfully.`),
duration: 5000,
});
} catch (err) {
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 enable this user.`)
.otherwise(() => msg`An error occurred while enabling the user.`);
toast({
title: _(msg`Error`),
description: _(errorMessage),
variant: 'destructive',
duration: 7500,
});
}
};
return (
<div className={className}>
<Alert
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<div>
<AlertTitle>Enable Account</AlertTitle>
<AlertDescription className="mr-2">
<Trans>
Enabling the account results in the user being able to use the account again, and all
the related features such as webhooks, teams, and API keys for example.
</Trans>
</AlertDescription>
</div>
<div className="flex-shrink-0">
<Dialog>
<DialogTrigger asChild>
<Button>
<Trans>Enable Account</Trans>
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader className="space-y-4">
<DialogTitle>
<Trans>Enable Account</Trans>
</DialogTitle>
</DialogHeader>
<div>
<DialogDescription>
<Trans>
To confirm, please enter the accounts email address <br />({userToEnable.email}
).
</Trans>
</DialogDescription>
<Input
className="mt-2"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<DialogFooter>
<Button
onClick={onEnableAccount}
loading={isEnablingUser}
disabled={email !== userToEnable.email}
>
<Trans>Enable account</Trans>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</Alert>
</div>
);
};

View File

@ -23,6 +23,8 @@ import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { DeleteUserDialog } from './delete-user-dialog';
import { DisableUserDialog } from './disable-user-dialog';
import { EnableUserDialog } from './enable-user-dialog';
import { MultiSelectRoleCombobox } from './multiselect-role-combobox';
const ZUserFormSchema = ZAdminUpdateProfileMutationSchema.omit({ id: true });
@ -35,7 +37,7 @@ export default function UserPage({ params }: { params: { id: number } }) {
const router = useRouter();
const { data: user } = trpc.profile.getUser.useQuery(
const { data: user } = trpc.admin.getUser.useQuery(
{
id: Number(params.id),
},
@ -153,7 +155,11 @@ export default function UserPage({ params }: { params: { id: number } }) {
<hr className="my-4" />
{user && <DeleteUserDialog user={user} />}
<div className="flex flex-col items-center gap-4">
{user && <DeleteUserDialog user={user} />}
{user && user.disabled && <EnableUserDialog userToEnable={user} />}
{user && !user.disabled && <DisableUserDialog userToDisable={user} />}
</div>
</div>
);
}

View File

@ -47,7 +47,7 @@ export const DocumentPageViewButton = ({ document, team }: DocumentPageViewButto
const onDownloadClick = async () => {
try {
const documentWithData = await trpcClient.document.getDocumentById.query({
id: document.id,
documentId: document.id,
teamId: team?.id,
});

View File

@ -75,7 +75,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
const onDownloadClick = async () => {
try {
const documentWithData = await trpcClient.document.getDocumentById.query({
id: document.id,
documentId: document.id,
teamId: team?.id,
});

View File

@ -37,10 +37,8 @@ export const DocumentPageViewRecentActivity = ({
{
documentId,
filterForRecentActivity: true,
orderBy: {
column: 'createdAt',
direction: 'asc',
},
orderByColumn: 'createdAt',
orderByDirection: 'asc',
perPage: 10,
},
{

View File

@ -60,7 +60,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
const { user } = await getRequiredServerComponentSession();
const document = await getDocumentById({
id: documentId,
documentId,
userId: user.id,
teamId: team?.id,
}).catch(() => null);
@ -221,7 +221,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
<DocumentPageViewDropdown document={documentWithRecipients} team={team} />
</div>
<p className="text-muted-foreground mt-2 px-4 text-sm ">
<p className="text-muted-foreground mt-2 px-4 text-sm">
{match(document.status)
.with(DocumentStatus.COMPLETED, () => (
<Trans>This document has been signed by all recipients</Trans>

View File

@ -12,8 +12,8 @@ import {
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
SKIP_QUERY_BATCH_META,
} from '@documenso/lib/constants/trpc';
import type { TGetDocumentWithDetailsByIdResponse } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import { DocumentDistributionMethod, DocumentStatus } from '@documenso/prisma/client';
import type { DocumentWithDetails } from '@documenso/prisma/types/document';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -35,7 +35,7 @@ import { useOptionalCurrentTeam } from '~/providers/team';
export type EditDocumentFormProps = {
className?: string;
initialDocument: DocumentWithDetails;
initialDocument: TGetDocumentWithDetailsByIdResponse;
documentRootPath: string;
isDocumentEnterprise: boolean;
};
@ -63,7 +63,7 @@ export const EditDocumentForm = ({
const { data: document, refetch: refetchDocument } =
trpc.document.getDocumentWithDetailsById.useQuery(
{
id: initialDocument.id,
documentId: initialDocument.id,
teamId: team?.id,
},
{
@ -79,7 +79,7 @@ export const EditDocumentForm = ({
onSuccess: (newData) => {
utils.document.getDocumentWithDetailsById.setData(
{
id: initialDocument.id,
documentId: initialDocument.id,
teamId: team?.id,
},
(oldData) => ({ ...(oldData || initialDocument), ...newData }),
@ -93,7 +93,7 @@ export const EditDocumentForm = ({
onSuccess: (newData) => {
utils.document.getDocumentWithDetailsById.setData(
{
id: initialDocument.id,
documentId: initialDocument.id,
teamId: team?.id,
},
(oldData) => ({ ...(oldData || initialDocument), ...newData, id: Number(newData.id) }),
@ -103,10 +103,10 @@ export const EditDocumentForm = ({
const { mutateAsync: addFields } = trpc.field.addFields.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newFields) => {
onSuccess: ({ fields: newFields }) => {
utils.document.getDocumentWithDetailsById.setData(
{
id: initialDocument.id,
documentId: initialDocument.id,
teamId: team?.id,
},
(oldData) => ({ ...(oldData || initialDocument), Field: newFields }),
@ -120,7 +120,7 @@ export const EditDocumentForm = ({
onSuccess: (newData) => {
utils.document.getDocumentWithDetailsById.setData(
{
id: initialDocument.id,
documentId: initialDocument.id,
teamId: team?.id,
},
(oldData) => ({
@ -132,12 +132,12 @@ export const EditDocumentForm = ({
},
});
const { mutateAsync: addSigners } = trpc.recipient.addSigners.useMutation({
const { mutateAsync: addSigners } = trpc.recipient.setDocumentRecipients.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newRecipients) => {
onSuccess: ({ recipients: newRecipients }) => {
utils.document.getDocumentWithDetailsById.setData(
{
id: initialDocument.id,
documentId: initialDocument.id,
teamId: team?.id,
},
(oldData) => ({ ...(oldData || initialDocument), Recipient: newRecipients }),
@ -150,7 +150,7 @@ export const EditDocumentForm = ({
onSuccess: (newData) => {
utils.document.getDocumentWithDetailsById.setData(
{
id: initialDocument.id,
documentId: initialDocument.id,
teamId: team?.id,
},
(oldData) => ({ ...(oldData || initialDocument), ...newData }),

View File

@ -41,7 +41,7 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
const { user } = await getRequiredServerComponentSession();
const document = await getDocumentWithDetailsById({
id: documentId,
documentId,
userId: user.id,
teamId: team?.id,
}).catch(() => null);

View File

@ -11,7 +11,7 @@ import type { DateTimeFormatOptions } from 'luxon';
import { UAParser } from 'ua-parser-js';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
import { trpc } from '@documenso/trpc/react';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
@ -35,9 +35,7 @@ export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps
const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
Object.fromEntries(searchParams ?? []),
);
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
const { data, isLoading, isInitialLoading, isLoadingError } =
trpc.document.findDocumentAuditLogs.useQuery(

View File

@ -47,7 +47,7 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
const [document, recipients] = await Promise.all([
getDocumentById({
id: documentId,
documentId,
userId: user.id,
teamId: team?.id,
}).catch(() => null),

View File

@ -54,7 +54,7 @@ export const DataTableActionButton = ({ row, team }: DataTableActionButtonProps)
if (!recipient) {
document = await trpcClient.document.getDocumentById.query({
id: row.id,
documentId: row.id,
teamId: team?.id,
});
} else {

View File

@ -85,7 +85,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
if (!recipient) {
document = await trpcClient.document.getDocumentById.query({
id: row.id,
documentId: row.id,
teamId: team?.id,
});
} else {

View File

@ -9,8 +9,8 @@ import { DateTime } from 'luxon';
import { useSession } from 'next-auth/react';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import type { FindResultSet } from '@documenso/lib/types/find-result-set';
import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
import type { TFindDocumentsResponse } from '@documenso/lib/server-only/document/find-documents';
import type { Team } from '@documenso/prisma/client';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
@ -24,13 +24,7 @@ import { DataTableActionDropdown } from './data-table-action-dropdown';
import { DataTableTitle } from './data-table-title';
export type DocumentsDataTableProps = {
results: FindResultSet<
Document & {
Recipient: Recipient[];
User: Pick<User, 'id' | 'name' | 'email'>;
team: Pick<Team, 'id' | 'url'> | null;
}
>;
results: TFindDocumentsResponse;
showSenderColumn?: boolean;
team?: Pick<Team, 'id' | 'url'> & { teamEmail?: string };
};

View File

@ -76,7 +76,7 @@ export const DeleteDocumentDialog = ({
const onDelete = async () => {
try {
await deleteDocument({ id, teamId });
await deleteDocument({ documentId: id, teamId });
} catch {
toast({
title: _(msg`Something went wrong`),

View File

@ -83,7 +83,7 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
perPage,
period,
senderIds,
search,
query: search,
});
const getTabHref = (value: typeof status) => {

View File

@ -36,7 +36,7 @@ export const DuplicateDocumentDialog = ({
const { _ } = useLingui();
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({
id,
documentId: id,
teamId: team?.id,
});
@ -66,7 +66,7 @@ export const DuplicateDocumentDialog = ({
const onDuplicate = async () => {
try {
await duplicateDocument({ id, teamId: team?.id });
await duplicateDocument({ documentId: id, teamId: team?.id });
} catch {
toast({
title: _(msg`Something went wrong`),
@ -92,7 +92,7 @@ export const DuplicateDocumentDialog = ({
</h1>
</div>
) : (
<div className="p-2 [&>div]:h-[50vh] [&>div]:overflow-y-scroll ">
<div className="p-2 [&>div]:h-[50vh] [&>div]:overflow-y-scroll">
<LazyPDFViewer key={document?.id} documentData={documentData} />
</div>
)}

View File

@ -7,7 +7,6 @@ import { useLingui } from '@lingui/react';
import { signOut } from 'next-auth/react';
import type { User } from '@documenso/prisma/client';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
@ -52,30 +51,20 @@ export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProp
return await signOut({ callbackUrl: '/' });
} catch (err) {
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
toast({
title: _(msg`An error occurred`),
description: err.message,
variant: 'destructive',
});
} else {
toast({
title: _(msg`An unknown error occurred`),
variant: 'destructive',
description:
err.message ??
_(
msg`We encountered an unknown error while attempting to delete your account. Please try again later.`,
),
});
}
toast({
title: _(msg`An unknown error occurred`),
variant: 'destructive',
description: _(
msg`We encountered an unknown error while attempting to delete your account. Please try again later.`,
),
});
}
};
return (
<div className={className}>
<Alert
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row "
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<div>

View File

@ -75,7 +75,7 @@ export const PublicProfilePageView = ({ user, team, profile }: PublicProfilePage
const enabledPrivateDirectTemplates = useMemo(
() =>
(data?.templates ?? []).filter(
(data?.data ?? []).filter(
(template): template is DirectTemplate =>
template.directLink?.enabled === true && template.type !== TemplateType.PUBLIC,
),

View File

@ -52,7 +52,7 @@ export const PublicTemplatesDataTable = () => {
);
const { directTemplates, publicDirectTemplates, privateDirectTemplates } = useMemo(() => {
const directTemplates = (data?.templates ?? []).filter(
const directTemplates = (data?.data ?? []).filter(
(template): template is DirectTemplate => template.directLink?.enabled === true,
);

View File

@ -12,7 +12,7 @@ import { UAParser } from 'ua-parser-js';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { USER_SECURITY_AUDIT_LOG_MAP } from '@documenso/lib/constants/auth';
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
import { trpc } from '@documenso/trpc/react';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
@ -33,9 +33,7 @@ export const UserSecurityActivityDataTable = () => {
const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
Object.fromEntries(searchParams ?? []),
);
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
const { data, isLoading, isInitialLoading, isLoadingError } =
trpc.profile.findUserSecurityAuditLogs.useQuery(

View File

@ -9,7 +9,7 @@ import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
import { trpc } from '@documenso/trpc/react';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
@ -27,9 +27,7 @@ export const UserPasskeysDataTable = () => {
const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
Object.fromEntries(searchParams ?? []),
);
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
const { data, isLoading, isInitialLoading, isLoadingError } = trpc.auth.findPasskeys.useQuery(
{

View File

@ -59,16 +59,16 @@ export const EditTemplateForm = ({
const utils = trpc.useUtils();
const { data: template, refetch: refetchTemplate } =
trpc.template.getTemplateWithDetailsById.useQuery(
{
id: initialTemplate.id,
},
{
initialData: initialTemplate,
...SKIP_QUERY_BATCH_META,
},
);
const { data: template, refetch: refetchTemplate } = trpc.template.getTemplateById.useQuery(
{
templateId: initialTemplate.id,
teamId: initialTemplate.teamId || undefined,
},
{
initialData: initialTemplate,
...SKIP_QUERY_BATCH_META,
},
);
const { Recipient: recipients, Field: fields, templateDocumentData } = template;
@ -92,12 +92,12 @@ export const EditTemplateForm = ({
const currentDocumentFlow = documentFlow[step];
const { mutateAsync: updateTemplateSettings } = trpc.template.updateTemplateSettings.useMutation({
const { mutateAsync: updateTemplateSettings } = trpc.template.updateTemplate.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.template.getTemplateWithDetailsById.setData(
utils.template.getTemplateById.setData(
{
id: initialTemplate.id,
templateId: initialTemplate.id,
},
(oldData) => ({ ...(oldData || initialTemplate), ...newData }),
);
@ -108,9 +108,9 @@ export const EditTemplateForm = ({
trpc.template.setSigningOrderForTemplate.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.template.getTemplateWithDetailsById.setData(
utils.template.getTemplateById.setData(
{
id: initialTemplate.id,
templateId: initialTemplate.id,
},
(oldData) => ({ ...(oldData || initialTemplate), ...newData }),
);
@ -120,9 +120,9 @@ export const EditTemplateForm = ({
const { mutateAsync: addTemplateFields } = trpc.field.addTemplateFields.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.template.getTemplateWithDetailsById.setData(
utils.template.getTemplateById.setData(
{
id: initialTemplate.id,
templateId: initialTemplate.id,
},
(oldData) => ({ ...(oldData || initialTemplate), ...newData }),
);
@ -132,9 +132,9 @@ export const EditTemplateForm = ({
const { mutateAsync: addTemplateSigners } = trpc.recipient.addTemplateSigners.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.template.getTemplateWithDetailsById.setData(
utils.template.getTemplateById.setData(
{
id: initialTemplate.id,
templateId: initialTemplate.id,
},
(oldData) => ({ ...(oldData || initialTemplate), ...newData }),
);
@ -145,9 +145,9 @@ export const EditTemplateForm = ({
trpc.template.updateTemplateTypedSignatureSettings.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.template.getTemplateWithDetailsById.setData(
utils.template.getTemplateById.setData(
{
id: initialTemplate.id,
templateId: initialTemplate.id,
},
(oldData) => ({
...(oldData || initialTemplate),

View File

@ -8,7 +8,7 @@ import { ChevronLeft } from 'lucide-react';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getTemplateWithDetailsById } from '@documenso/lib/server-only/template/get-template-with-details-by-id';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
import type { Team } from '@documenso/prisma/client';
@ -37,9 +37,10 @@ export const TemplateEditPageView = async ({ params, team }: TemplateEditPageVie
const { user } = await getRequiredServerComponentSession();
const template = await getTemplateWithDetailsById({
const template = await getTemplateById({
id: templateId,
userId: user.id,
teamId: team?.id,
}).catch(() => null);
if (!template || !template.templateDocumentData) {

View File

@ -12,7 +12,7 @@ import { DateTime } from 'luxon';
import { z } from 'zod';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
import type { Team } from '@documenso/prisma/client';
import { DocumentSource, DocumentStatus as DocumentStatusEnum } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
@ -40,7 +40,7 @@ const DOCUMENT_SOURCE_LABELS: { [key in DocumentSource]: MessageDescriptor } = {
TEMPLATE_DIRECT_LINK: msg`Direct link`,
};
const ZTemplateSearchParamsSchema = ZBaseTableSearchParamsSchema.extend({
const ZDocumentSearchParamsSchema = ZUrlSearchParamsSchema.extend({
source: z
.nativeEnum(DocumentSource)
.optional()
@ -49,10 +49,6 @@ const ZTemplateSearchParamsSchema = ZBaseTableSearchParamsSchema.extend({
.nativeEnum(DocumentStatusEnum)
.optional()
.catch(() => undefined),
search: z.coerce
.string()
.optional()
.catch(() => undefined),
});
type TemplatePageViewDocumentsTableProps = {
@ -69,7 +65,7 @@ export const TemplatePageViewDocumentsTable = ({
const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const parsedSearchParams = ZTemplateSearchParamsSchema.parse(
const parsedSearchParams = ZDocumentSearchParamsSchema.parse(
Object.fromEntries(searchParams ?? []),
);
@ -80,7 +76,7 @@ export const TemplatePageViewDocumentsTable = ({
teamId: team?.id,
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
search: parsedSearchParams.search,
query: parsedSearchParams.query,
source: parsedSearchParams.source,
status: parsedSearchParams.status,
},

View File

@ -26,10 +26,8 @@ export const TemplatePageViewRecentActivity = ({
const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocuments.useQuery({
templateId,
teamId,
orderBy: {
column: 'createdAt',
direction: 'asc',
},
orderByColumn: 'createdAt',
orderByDirection: 'asc',
perPage: 5,
});

View File

@ -85,7 +85,7 @@ export const DeleteTemplateDialog = ({
type="button"
variant="destructive"
loading={isLoading}
onClick={async () => deleteTemplate({ id, teamId })}
onClick={async () => deleteTemplate({ templateId: id, teamId })}
>
<Trans>Delete</Trans>
</Button>

View File

@ -4,8 +4,10 @@ import { useRouter } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { match } from 'ts-pattern';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button';
@ -51,10 +53,20 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
});
onOpenChange(false);
},
onError: (error) => {
onError: (err) => {
const error = AppError.parseError(err);
const errorMessage = match(error.code)
.with(
AppErrorCode.NOT_FOUND,
() => msg`Template not found or already associated with a team.`,
)
.with(AppErrorCode.UNAUTHORIZED, () => msg`You are not a member of this team.`)
.otherwise(() => msg`An error occurred while moving the template.`);
toast({
title: _(msg`Error`),
description: error.message || _(msg`An error occurred while moving the template.`),
description: _(errorMessage),
variant: 'destructive',
duration: 7500,
});

View File

@ -29,7 +29,7 @@ export const TemplatesPageView = async ({ searchParams = {}, team }: TemplatesPa
const documentRootPath = formatDocumentsPath(team?.url);
const templateRootPath = formatTemplatesPath(team?.url);
const { templates, totalPages } = await findTemplates({
const { data: templates, totalPages } = await findTemplates({
userId: user.id,
teamId: team?.id,
page: page,

View File

@ -9,7 +9,7 @@ import { Trans } from '@lingui/macro';
import { PlusIcon } from 'lucide-react';
import LogoIcon from '@documenso/assets/logo_icon.png';
import type { GetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
import type { User } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@ -19,7 +19,7 @@ import { Logo } from '~/components/branding/logo';
type ProfileHeaderProps = {
user?: User | null;
teams?: GetTeamsResponse;
teams?: TGetTeamsResponse;
};
export const ProfileHeader = ({ user, teams = [] }: ProfileHeaderProps) => {
@ -58,7 +58,7 @@ export const ProfileHeader = ({ user, teams = [] }: ProfileHeaderProps) => {
alt="Documenso Logo"
width={48}
height={48}
className="h-10 w-auto dark:invert sm:hidden"
className="h-10 w-auto sm:hidden dark:invert"
/>
</Link>

View File

@ -40,7 +40,7 @@ export type TConfigureDirectTemplateFormSchema = z.infer<typeof ZConfigureDirect
export type ConfigureDirectTemplateFormProps = {
flowStep: DocumentFlowStep;
isDocumentPdfLoaded: boolean;
template: TemplateWithDetails;
template: Omit<TemplateWithDetails, 'User'>;
directTemplateRecipient: Recipient & { Field: Field[] };
initialEmail?: string;
onSubmit: (_data: TConfigureDirectTemplateFormSchema) => void;

View File

@ -28,7 +28,7 @@ import type { DirectTemplateLocalField } from './sign-direct-template';
import { SignDirectTemplateForm } from './sign-direct-template';
export type TemplatesDirectPageViewProps = {
template: TemplateWithDetails;
template: Omit<TemplateWithDetails, 'User'>;
directTemplateToken: string;
directTemplateRecipient: Recipient & { Field: Field[] };
};

View File

@ -1,7 +1,6 @@
import { useMemo, useState } from 'react';
import { Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
@ -56,7 +55,7 @@ export type SignDirectTemplateFormProps = {
flowStep: DocumentFlowStep;
directRecipient: Recipient;
directRecipientFields: Field[];
template: TemplateWithDetails;
template: Omit<TemplateWithDetails, 'User'>;
onSubmit: (_data: DirectTemplateLocalField[]) => Promise<void>;
};
@ -72,9 +71,8 @@ export const SignDirectTemplateForm = ({
template,
onSubmit,
}: SignDirectTemplateFormProps) => {
const { _ } = useLingui();
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
const { fullName, signature, signatureValid, setFullName, setSignature } =
useRequiredSigningContext();
const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(directRecipientFields);
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
@ -135,6 +133,8 @@ export const SignDirectTemplateForm = ({
);
};
const hasSignatureField = localFields.some((field) => field.type === FieldType.SIGNATURE);
const uninsertedFields = useMemo(() => {
return sortFieldsByPosition(localFields.filter((field) => !field.inserted));
}, [localFields]);
@ -147,6 +147,10 @@ export const SignDirectTemplateForm = ({
const handleSubmit = async () => {
setValidateUninsertedFields(true);
if (hasSignatureField && !signatureValid) {
return;
}
const isFieldsValid = validateFieldsInserted(localFields);
if (!isFieldsValid) {

View File

@ -2,7 +2,7 @@ import React from 'react';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import type { GetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
import { getTeams } from '@documenso/lib/server-only/team/get-teams';
import { Header as AuthenticatedHeader } from '~/components/(dashboard)/layout/header';
@ -23,7 +23,7 @@ export default async function RecipientLayout({ children }: RecipientLayoutProps
const { user, session } = await getServerComponentSession();
let teams: GetTeamsResponse = [];
let teams: TGetTeamsResponse = [];
if (user && session) {
teams = await getTeams({ userId: user.id });

View File

@ -12,6 +12,7 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/tr
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
import { fromCheckboxValue, toCheckboxValue } from '@documenso/lib/universal/field-checkbox';
import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import { trpc } from '@documenso/trpc/react';
@ -54,6 +55,7 @@ export const CheckboxField = ({
...item,
value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
}));
const [checkedValues, setCheckedValues] = useState(
values
?.map((item) =>
@ -97,7 +99,7 @@ export const CheckboxField = ({
const payload: TSignFieldWithTokenMutationSchema = {
token: recipient.token,
fieldId: field.id,
value: checkedValues.join(','),
value: toCheckboxValue(checkedValues),
isBase64: true,
authOptions,
};
@ -191,7 +193,7 @@ export const CheckboxField = ({
await signFieldWithToken({
token: recipient.token,
fieldId: field.id,
value: updatedValues.join(','),
value: toCheckboxValue(checkedValues),
isBase64: true,
});
}
@ -228,6 +230,11 @@ export const CheckboxField = ({
}
}, [checkedValues, isLengthConditionMet, field.inserted]);
const parsedCheckedValues = useMemo(
() => fromCheckboxValue(field.customText),
[field.customText],
);
return (
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Checkbox">
{isLoading && (
@ -277,9 +284,7 @@ export const CheckboxField = ({
className="h-3 w-3"
checkClassName="text-white"
id={`checkbox-${index}`}
checked={field.customText
.split(',')
.some((customValue) => customValue === itemValue)}
checked={parsedCheckedValues.includes(itemValue)}
disabled={isLoading}
onCheckedChange={() => void handleCheckboxOptionClick(item)}
/>

View File

@ -9,7 +9,7 @@ import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { TRPCClientError } from '@documenso/trpc/client';
import { AppError } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { Button } from '@documenso/ui/primitives/button';
@ -25,6 +25,8 @@ import { Input } from '@documenso/ui/primitives/input';
import { PasswordInput } from '@documenso/ui/primitives/password-input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { signupErrorMessages } from '~/components/forms/v2/signup';
export type ClaimAccountProps = {
defaultName: string;
defaultEmail: string;
@ -33,7 +35,10 @@ export type ClaimAccountProps = {
export const ZClaimAccountFormSchema = z
.object({
name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
name: z
.string()
.trim()
.min(1, { message: msg`Please enter a valid name.`.id }),
email: z.string().email().min(1),
password: ZPasswordSchema,
})
@ -43,7 +48,7 @@ export const ZClaimAccountFormSchema = z
return !password.includes(name) && !password.includes(email.split('@')[0]);
},
{
message: 'Password should not be common or based on personal information',
message: msg`Password should not be common or based on personal information`.id,
path: ['password'],
},
);
@ -86,22 +91,16 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
email,
timestamp: new Date().toISOString(),
});
} catch (error) {
if (error instanceof TRPCClientError && error.data?.code === 'BAD_REQUEST') {
toast({
title: _(msg`An error occurred`),
description: error.message,
variant: 'destructive',
});
} else {
toast({
title: _(msg`An unknown error occurred`),
description: _(
msg`We encountered an unknown error while attempting to sign you up. Please try again later.`,
),
variant: 'destructive',
});
}
} catch (err) {
const error = AppError.parseError(err);
const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST;
toast({
title: _(msg`An error occurred`),
description: _(errorMessage),
variant: 'destructive',
});
}
};

View File

@ -178,7 +178,7 @@ export const DropdownField = ({
)}
{!field.inserted && (
<p className="group-hover:text-primary text-muted-foreground flex flex-col items-center justify-center duration-200 ">
<p className="group-hover:text-primary text-muted-foreground flex flex-col items-center justify-center duration-200">
<Select value={localChoice} onValueChange={handleSelectItem}>
<SelectTrigger
className={cn(

View File

@ -11,8 +11,9 @@ import { useForm } from 'react-hook-form';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
import { type Field, type Recipient, RecipientRole } from '@documenso/prisma/client';
import { type Field, FieldType, type Recipient, RecipientRole } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { cn } from '@documenso/ui/lib/utils';
@ -44,7 +45,8 @@ export const SigningForm = ({
const analytics = useAnalytics();
const { data: session } = useSession();
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
const { fullName, signature, setFullName, setSignature, signatureValid, setSignatureValid } =
useRequiredSigningContext();
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
@ -56,19 +58,30 @@ export const SigningForm = ({
// Keep the loading state going if successful since the redirect may take some time.
const isSubmitting = formState.isSubmitting || formState.isSubmitSuccessful;
const fieldsRequiringValidation = useMemo(
() => fields.filter(isFieldUnsignedAndRequired),
[fields],
);
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
const uninsertedFields = useMemo(() => {
return sortFieldsByPosition(fields.filter((field) => !field.inserted));
return sortFieldsByPosition(fieldsRequiringValidation.filter((field) => !field.inserted));
}, [fields]);
const fieldsValidated = () => {
setValidateUninsertedFields(true);
validateFieldsInserted(fields);
validateFieldsInserted(fieldsRequiringValidation);
};
const onFormSubmit = async () => {
setValidateUninsertedFields(true);
const isFieldsValid = validateFieldsInserted(fields);
const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation);
if (hasSignatureField && !signatureValid) {
return;
}
if (!isFieldsValid) {
return;
@ -142,7 +155,7 @@ export const SigningForm = ({
<div className="flex flex-col gap-4 md:flex-row">
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
variant="secondary"
size="lg"
disabled={typeof window !== 'undefined' && window.history.length <= 1}
@ -198,20 +211,33 @@ export const SigningForm = ({
className="h-44 w-full"
disabled={isSubmitting}
defaultValue={signature ?? undefined}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
onChange={(value) => {
setSignature(value);
if (signatureValid) {
setSignature(value);
}
}}
allowTypedSignature={document.documentMeta?.typedSignatureEnabled}
/>
</CardContent>
</Card>
{hasSignatureField && !signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>
Signature is too small. Please provide a more complete signature.
</Trans>
</div>
)}
</div>
</div>
<div className="flex flex-col gap-4 md:flex-row">
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
variant="secondary"
size="lg"
disabled={typeof window !== 'undefined' && window.history.length <= 1}

View File

@ -2,7 +2,7 @@ import React from 'react';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import type { GetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
import { getTeams } from '@documenso/lib/server-only/team/get-teams';
import { Header as AuthenticatedHeader } from '~/components/(dashboard)/layout/header';
@ -17,7 +17,7 @@ export default async function SigningLayout({ children }: SigningLayoutProps) {
const { user, session } = await getServerComponentSession();
let teams: GetTeamsResponse = [];
let teams: TGetTeamsResponse = [];
if (user && session) {
teams = await getTeams({ userId: user.id });

View File

@ -205,7 +205,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
<div className="flex w-full flex-1 flex-nowrap gap-4">
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
variant="secondary"
onClick={() => {
setShowFullNameModal(false);

View File

@ -318,7 +318,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
<div className="flex w-full flex-1 flex-nowrap gap-4">
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
variant="secondary"
onClick={() => {
setShowRadioModal(false);

View File

@ -9,6 +9,8 @@ export type SigningContextValue = {
setEmail: (_value: string) => void;
signature: string | null;
setSignature: (_value: string | null) => void;
signatureValid: boolean;
setSignatureValid: (_valid: boolean) => void;
};
const SigningContext = createContext<SigningContextValue | null>(null);
@ -43,6 +45,7 @@ export const SigningProvider = ({
const [fullName, setFullName] = useState(initialFullName || '');
const [email, setEmail] = useState(initialEmail || '');
const [signature, setSignature] = useState(initialSignature || null);
const [signatureValid, setSignatureValid] = useState(true);
useEffect(() => {
if (initialSignature) {
@ -59,6 +62,8 @@ export const SigningProvider = ({
setEmail,
signature,
setSignature,
signatureValid,
setSignatureValid,
}}
>
{children}

View File

@ -1,7 +1,8 @@
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { Trans } from '@lingui/macro';
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
import type { Field } from '@documenso/prisma/client';
import { RecipientRole } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
@ -36,7 +37,7 @@ export const SignDialog = ({
}: SignDialogProps) => {
const [showDialog, setShowDialog] = useState(false);
const isComplete = fields.every((field) => field.inserted);
const isComplete = useMemo(() => !fieldsContainUnsignedRequiredField(fields), [fields]);
const handleOpenChange = (open: boolean) => {
if (isSubmitting || !isComplete) {

View File

@ -55,8 +55,12 @@ export const SignatureField = ({
const containerRef = useRef<HTMLDivElement>(null);
const [fontSize, setFontSize] = useState(2);
const { signature: providedSignature, setSignature: setProvidedSignature } =
useRequiredSigningContext();
const {
signature: providedSignature,
setSignature: setProvidedSignature,
signatureValid,
setSignatureValid,
} = useRequiredSigningContext();
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
@ -90,7 +94,7 @@ export const SignatureField = ({
}, [field.inserted, signature?.signatureImageAsBase64]);
const onPreSign = () => {
if (!providedSignature) {
if (!providedSignature || !signatureValid) {
setShowSignatureModal(true);
return false;
}
@ -117,7 +121,7 @@ export const SignatureField = ({
try {
const value = signature || providedSignature;
if (!value) {
if (!value || (signature && !signatureValid)) {
setShowSignatureModal(true);
return;
}
@ -282,12 +286,23 @@ export const SignatureField = ({
<Trans>Signature</Trans>
</Label>
<SignaturePad
id="signature"
className="border-border mt-2 h-44 w-full rounded-md border"
onChange={(value) => setLocalSignature(value)}
allowTypedSignature={typedSignatureEnabled}
/>
<div className="border-border mt-2 rounded-md border">
<SignaturePad
id="signature"
className="h-44 w-full"
onChange={(value) => setLocalSignature(value)}
allowTypedSignature={typedSignatureEnabled}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
/>
</div>
{!signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>Signature is too small. Please provide a more complete signature.</Trans>
</div>
)}
</div>
<SigningDisclosure />
@ -307,7 +322,7 @@ export const SignatureField = ({
<Button
type="button"
className="flex-1"
disabled={!localSignature}
disabled={!localSignature || !signatureValid}
onClick={() => onDialogSignClick()}
>
<Trans>Sign</Trans>

View File

@ -214,15 +214,15 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
parsedField?.label && parsedField.label.length < 20
? parsedField.label
: parsedField?.label
? parsedField?.label.substring(0, 20) + '...'
: undefined;
? parsedField?.label.substring(0, 20) + '...'
: undefined;
const textDisplay =
parsedField?.text && parsedField.text.length < 20
? parsedField.text
: parsedField?.text
? parsedField?.text.substring(0, 20) + '...'
: undefined;
? parsedField?.text.substring(0, 20) + '...'
: undefined;
const fieldDisplayName = labelDisplay ? labelDisplay : textDisplay;
const charactersRemaining = (parsedFieldMeta?.characterLimit ?? 0) - (localText.length ?? 0);
@ -325,7 +325,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
<div className="mt-4 flex w-full flex-1 flex-nowrap gap-4">
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
variant="secondary"
onClick={() => {
setShowCustomTextModal(false);

View File

@ -48,7 +48,7 @@ export default async function WaitingForTurnToSignPage({
if (user) {
isOwnerOrTeamMember = await getDocumentById({
id: document.id,
documentId: document.id,
userId: user.id,
teamId: document.teamId ?? undefined,
})

View File

@ -2,6 +2,7 @@ import { Plural, Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
import type Stripe from 'stripe';
import { match } from 'ts-pattern';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
@ -44,15 +45,24 @@ export default async function TeamsSettingBillingPage({ params }: TeamsSettingsB
const numberOfSeats = subscription.items.data[0].quantity ?? 0;
const formattedTeamMemberQuanity = (
<Plural value={numberOfSeats} one="# member" other="# members" />
);
const formattedDate = DateTime.fromSeconds(subscription.current_period_end).toFormat(
'LLL dd, yyyy',
);
return _(msg`${formattedTeamMemberQuanity} • Monthly • Renews: ${formattedDate}`);
const subscriptionInterval = match(subscription?.items.data[0].plan.interval)
.with('year', () => _(msg`Yearly`))
.with('month', () => _(msg`Monthly`))
.otherwise(() => _(msg`Unknown`));
return (
<span>
<Plural value={numberOfSeats} one="# member" other="# members" />
{' • '}
<span>{subscriptionInterval}</span>
{' • '}
<Trans>Renews: {formattedDate}</Trans>
</span>
);
};
return (
@ -66,10 +76,6 @@ export default async function TeamsSettingBillingPage({ params }: TeamsSettingsB
<CardContent className="flex flex-row items-center justify-between p-4">
<div className="flex flex-col text-sm">
<p className="text-foreground font-semibold">
<Trans>Current plan: {teamSubscription ? 'Team' : 'Early Adopter Team'}</Trans>
</p>
<p className="text-muted-foreground mt-0.5">
{formatTeamSubscriptionDetails(teamSubscription)}
</p>
</div>

View File

@ -1,5 +1,5 @@
import { colord } from 'colord';
import { toSnakeCase } from 'remeda';
import { toKebabCase } from 'remeda';
import { z } from 'zod';
export const ZCssVarsSchema = z
@ -47,7 +47,7 @@ export const toNativeCssVars = (vars: TCssVarsSchema) => {
const color = colord(value);
const { h, s, l } = color.toHsl();
cssVars[`--${toSnakeCase(key)}`] = `${h} ${s} ${l}`;
cssVars[`--${toKebabCase(key)}`] = `${h} ${s} ${l}`;
}
}

View File

@ -67,8 +67,16 @@ export const EmbedDirectTemplateClientPage = ({
const searchParams = useSearchParams();
const { fullName, email, signature, setFullName, setEmail, setSignature } =
useRequiredSigningContext();
const {
fullName,
email,
signature,
signatureValid,
setFullName,
setEmail,
setSignature,
setSignatureValid,
} = useRequiredSigningContext();
const [hasFinishedInit, setHasFinishedInit] = useState(false);
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
@ -90,6 +98,8 @@ export const EmbedDirectTemplateClientPage = ({
localFields.filter((field) => field.inserted),
];
const hasSignatureField = localFields.some((field) => field.type === FieldType.SIGNATURE);
const { mutateAsync: createDocumentFromDirectTemplate, isLoading: isSubmitting } =
trpc.template.createDocumentFromDirectTemplate.useMutation();
@ -180,6 +190,10 @@ export const EmbedDirectTemplateClientPage = ({
const onCompleteClick = async () => {
try {
if (hasSignatureField && !signatureValid) {
return;
}
const valid = validateFieldsInserted(localFields);
if (!valid) {
@ -417,6 +431,9 @@ export const EmbedDirectTemplateClientPage = ({
onChange={(value) => {
setSignature(value);
}}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
allowTypedSignature={Boolean(
metadata &&
'typedSignatureEnabled' in metadata &&
@ -425,6 +442,14 @@ export const EmbedDirectTemplateClientPage = ({
/>
</CardContent>
</Card>
{hasSignatureField && !signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>
Signature is too small. Please provide a more complete signature.
</Trans>
</div>
)}
</div>
</div>
</div>

View File

@ -10,7 +10,7 @@ import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
import { type DocumentData, type Field } from '@documenso/prisma/client';
import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { Button } from '@documenso/ui/primitives/button';
@ -57,7 +57,15 @@ export const EmbedSignDocumentClientPage = ({
const { _ } = useLingui();
const { toast } = useToast();
const { fullName, email, signature, setFullName, setSignature } = useRequiredSigningContext();
const {
fullName,
email,
signature,
signatureValid,
setFullName,
setSignature,
setSignatureValid,
} = useRequiredSigningContext();
const [hasFinishedInit, setHasFinishedInit] = useState(false);
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
@ -79,6 +87,8 @@ export const EmbedSignDocumentClientPage = ({
const { mutateAsync: completeDocumentWithToken, isLoading: isSubmitting } =
trpc.recipient.completeDocumentWithToken.useMutation();
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
const onNextFieldClick = () => {
validateFieldsInserted(fields);
@ -88,6 +98,10 @@ export const EmbedSignDocumentClientPage = ({
const onCompleteClick = async () => {
try {
if (hasSignatureField && !signatureValid) {
return;
}
const valid = validateFieldsInserted(fields);
if (!valid) {
@ -296,6 +310,9 @@ export const EmbedSignDocumentClientPage = ({
onChange={(value) => {
setSignature(value);
}}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
allowTypedSignature={Boolean(
metadata &&
'typedSignatureEnabled' in metadata &&
@ -304,6 +321,14 @@ export const EmbedSignDocumentClientPage = ({
/>
</CardContent>
</Card>
{hasSignatureField && !signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>
Signature is too small. Please provide a more complete signature.
</Trans>
</div>
)}
</div>
</div>
</div>
@ -318,7 +343,7 @@ export const EmbedSignDocumentClientPage = ({
) : (
<Button
className="col-start-2"
disabled={isThrottled}
disabled={isThrottled || (hasSignatureField && !signatureValid)}
loading={isSubmitting}
onClick={() => throttledOnCompleteClick()}
>

View File

@ -47,10 +47,7 @@ export const StackAvatar = ({ first, zIndex, fallbackText = '', type }: StackAva
return (
<Avatar
className={`
${zIndexClass}
${firstClass}
dark:border-border h-10 w-10 border-2 border-solid border-white`}
className={` ${zIndexClass} ${firstClass} dark:border-border h-10 w-10 border-2 border-solid border-white`}
>
<AvatarFallback className={classes}>{fallbackText}</AvatarFallback>
</Avatar>

View File

@ -8,7 +8,7 @@ import { usePathname } from 'next/navigation';
import { MenuIcon, SearchIcon } from 'lucide-react';
import type { GetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
import { getRootHref } from '@documenso/lib/utils/params';
import type { User } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
@ -22,7 +22,7 @@ import { MobileNavigation } from './mobile-navigation';
export type HeaderProps = HTMLAttributes<HTMLDivElement> & {
user: User;
teams: GetTeamsResponse;
teams: TGetTeamsResponse;
};
export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
@ -75,7 +75,7 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
<div
className="flex gap-x-4 md:ml-8"
title={selectedTeam ? selectedTeam.name : user.name ?? ''}
title={selectedTeam ? selectedTeam.name : (user.name ?? '')}
>
<MenuSwitcher user={user} teams={teams} />
</div>

View File

@ -14,7 +14,7 @@ import { signOut } from 'next-auth/react';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { TEAM_MEMBER_ROLE_MAP, TEAM_URL_REGEX } from '@documenso/lib/constants/teams';
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
import type { GetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
import type { User } from '@documenso/prisma/client';
@ -36,7 +36,7 @@ const MotionLink = motion(Link);
export type MenuSwitcherProps = {
user: User;
teams: GetTeamsResponse;
teams: TGetTeamsResponse;
};
export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProps) => {

View File

@ -14,7 +14,7 @@ import type { z } from 'zod';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { ZCreateTeamEmailVerificationMutationSchema } from '@documenso/trpc/server/team-router/schema';
import { ZCreateTeamEmailVerificationRequestSchema } from '@documenso/trpc/server/team-router/create-team-email-verification-route';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
@ -41,7 +41,7 @@ export type AddTeamEmailDialogProps = {
trigger?: React.ReactNode;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
const ZCreateTeamEmailFormSchema = ZCreateTeamEmailVerificationMutationSchema.pick({
const ZCreateTeamEmailFormSchema = ZCreateTeamEmailVerificationRequestSchema.pick({
name: true,
email: true,
});

View File

@ -15,7 +15,7 @@ import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-upda
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { ZCreateTeamMutationSchema } from '@documenso/trpc/server/team-router/schema';
import { ZCreateTeamRequestSchema } from '@documenso/trpc/server/team-router/create-team-route';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
@ -41,7 +41,7 @@ export type CreateTeamDialogProps = {
trigger?: React.ReactNode;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
const ZCreateTeamFormSchema = ZCreateTeamMutationSchema.pick({
const ZCreateTeamFormSchema = ZCreateTeamRequestSchema.pick({
teamName: true,
teamUrl: true,
});

View File

@ -15,7 +15,7 @@ import { downloadFile } from '@documenso/lib/client-only/download-file';
import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
import { TeamMemberRole } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { ZCreateTeamMemberInvitesMutationSchema } from '@documenso/trpc/server/team-router/schema';
import { ZCreateTeamMemberInvitesRequestSchema } from '@documenso/trpc/server/team-router/create-team-member-invites-route';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -55,7 +55,7 @@ export type InviteTeamMembersDialogProps = {
const ZInviteTeamMembersFormSchema = z
.object({
invitations: ZCreateTeamMemberInvitesMutationSchema.shape.invitations,
invitations: ZCreateTeamMemberInvitesRequestSchema.shape.invitations,
})
// Display exactly which rows are duplicates.
.superRefine((items, ctx) => {

View File

@ -12,7 +12,7 @@ import type { z } from 'zod';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { ZUpdateTeamMutationSchema } from '@documenso/trpc/server/team-router/schema';
import { ZUpdateTeamRequestSchema } from '@documenso/trpc/server/team-router/update-team-route';
import { Button } from '@documenso/ui/primitives/button';
import {
Form,
@ -31,7 +31,7 @@ export type UpdateTeamDialogProps = {
teamUrl: string;
};
const ZUpdateTeamFormSchema = ZUpdateTeamMutationSchema.shape.data.pick({
const ZUpdateTeamFormSchema = ZUpdateTeamRequestSchema.shape.data.pick({
name: true,
url: true,
});

View File

@ -11,7 +11,7 @@ import { useLingui } from '@lingui/react';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { NEXT_PUBLIC_WEBAPP_URL, WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
@ -30,13 +30,11 @@ export const CurrentUserTeamsDataTable = () => {
const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
Object.fromEntries(searchParams ?? []),
);
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
const { data, isLoading, isInitialLoading, isLoadingError } = trpc.team.findTeams.useQuery(
{
term: parsedSearchParams.query,
query: parsedSearchParams.query,
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
},

View File

@ -9,7 +9,7 @@ import { useLingui } from '@lingui/react';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
import { trpc } from '@documenso/trpc/react';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
@ -27,15 +27,13 @@ export const PendingUserTeamsDataTable = () => {
const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
Object.fromEntries(searchParams ?? []),
);
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
const [checkoutPendingTeamId, setCheckoutPendingTeamId] = useState<number | null>(null);
const { data, isLoading, isInitialLoading, isLoadingError } = trpc.team.findTeamsPending.useQuery(
{
term: parsedSearchParams.query,
query: parsedSearchParams.query,
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
},

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