mirror of
https://github.com/documenso/documenso.git
synced 2025-11-22 04:31:39 +10:00
Merge branch 'main' into feat/org-insights
This commit is contained in:
57
AGENTS.md
Normal file
57
AGENTS.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# Agent Guidelines for Documenso
|
||||||
|
|
||||||
|
## Build/Test/Lint Commands
|
||||||
|
|
||||||
|
- `npm run build` - Build all packages
|
||||||
|
- `npm run lint` - Lint all packages
|
||||||
|
- `npm run lint:fix` - Auto-fix linting issues
|
||||||
|
- `npm run test:e2e` - Run E2E tests with Playwright
|
||||||
|
- `npm run test:dev -w @documenso/app-tests` - Run single E2E test in dev mode
|
||||||
|
- `npm run test-ui:dev -w @documenso/app-tests` - Run E2E tests with UI
|
||||||
|
- `npm run format` - Format code with Prettier
|
||||||
|
- `npm run dev` - Start development server for Remix app
|
||||||
|
|
||||||
|
## Code Style Guidelines
|
||||||
|
|
||||||
|
- Use TypeScript for all code; prefer `type` over `interface`
|
||||||
|
- Use functional components with `const Component = () => {}`
|
||||||
|
- Never use classes; prefer functional/declarative patterns
|
||||||
|
- Use descriptive variable names with auxiliary verbs (isLoading, hasError)
|
||||||
|
- Directory names: lowercase with dashes (auth-wizard)
|
||||||
|
- Use named exports for components
|
||||||
|
- Never use 'use client' directive
|
||||||
|
- Never use 1-line if statements
|
||||||
|
- Structure files: exported component, subcomponents, helpers, static content, types
|
||||||
|
|
||||||
|
## Error Handling & Validation
|
||||||
|
|
||||||
|
- Use custom AppError class when throwing errors
|
||||||
|
- When catching errors on the frontend use `const error = AppError.parse(error)` to get the error code
|
||||||
|
- Use early returns and guard clauses
|
||||||
|
- Use Zod for form validation and react-hook-form for forms
|
||||||
|
- Use error boundaries for unexpected errors
|
||||||
|
|
||||||
|
## UI & Styling
|
||||||
|
|
||||||
|
- Use Shadcn UI, Radix, and Tailwind CSS with mobile-first approach
|
||||||
|
- Use `<Form>` `<FormItem>` elements with fieldset having `:disabled` attribute when loading
|
||||||
|
- Use Lucide icons with longhand names (HomeIcon vs Home)
|
||||||
|
|
||||||
|
## TRPC Routes
|
||||||
|
|
||||||
|
- Each route in own file: `routers/teams/create-team.ts`
|
||||||
|
- Associated types file: `routers/teams/create-team.types.ts`
|
||||||
|
- Request/response schemas: `Z[RouteName]RequestSchema`, `Z[RouteName]ResponseSchema`
|
||||||
|
- Only use GET and POST methods in OpenAPI meta
|
||||||
|
- Deconstruct input argument on its own line
|
||||||
|
- Prefer route names such as get/getMany/find/create/update/delete
|
||||||
|
- "create" routes request schema should have the ID and data in the top level
|
||||||
|
- "update" routes request schema should have the ID in the top level and the data in a nested "data" object
|
||||||
|
|
||||||
|
## Translations & Remix
|
||||||
|
|
||||||
|
- Use `<Trans>string</Trans>` for JSX translations from `@lingui/react/macro`
|
||||||
|
- Use `t\`string\`` macro for TypeScript translations
|
||||||
|
- Use `(params: Route.Params)` and `(loaderData: Route.LoaderData)` for routes
|
||||||
|
- Directly return data from loaders, don't use `json()`
|
||||||
|
- Use `superLoaderJson` when sending complex data through loaders such as dates or prisma decimals
|
||||||
@ -214,8 +214,6 @@ For detailed instructions on how to configure and run the Docker container, plea
|
|||||||
|
|
||||||
We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates!
|
We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates!
|
||||||
|
|
||||||
> Please note that the below deployment methods are for v0.9, we will update these to v1.0 once it has been released.
|
|
||||||
|
|
||||||
### Fetch, configure, and build
|
### Fetch, configure, and build
|
||||||
|
|
||||||
First, clone the code from Github:
|
First, clone the code from Github:
|
||||||
@ -258,7 +256,7 @@ npm run start
|
|||||||
|
|
||||||
This will start the server on `localhost:3000`. For now, any reverse proxy can then do the frontend and SSL termination.
|
This will start the server on `localhost:3000`. For now, any reverse proxy can then do the frontend and SSL termination.
|
||||||
|
|
||||||
> If you want to run with another port than 3000, you can start the application with `next -p <ANY PORT>` from the `apps/web` folder.
|
> If you want to run with another port than 3000, you can start the application with `next -p <ANY PORT>` from the `apps/remix` folder.
|
||||||
|
|
||||||
### Run as a service
|
### Run as a service
|
||||||
|
|
||||||
@ -308,7 +306,7 @@ The Web UI can be found at http://localhost:9000, while the SMTP port will be on
|
|||||||
|
|
||||||
### Support IPv6
|
### Support IPv6
|
||||||
|
|
||||||
If you are deploying to a cluster that uses only IPv6, You can use a custom command to pass a parameter to the Next.js start command
|
If you are deploying to a cluster that uses only IPv6, You can use a custom command to pass a parameter to the Remix start command
|
||||||
|
|
||||||
For local docker run
|
For local docker run
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@ For the digital signature of your documents you need a signing certificate in .p
|
|||||||
|
|
||||||
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
|
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
|
||||||
|
|
||||||
5. Place the certificate `/apps/web/resources/certificate.p12` (If the path does not exist, it needs to be created)
|
5. Place the certificate `/apps/remix/resources/certificate.p12` (If the path does not exist, it needs to be created)
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,7 @@ The translation files are organized into folders represented by their respective
|
|||||||
Each PO file contains translations which look like this:
|
Each PO file contains translations which look like this:
|
||||||
|
|
||||||
```po
|
```po
|
||||||
#: apps/web/src/app/(signing)/sign/[token]/no-longer-available.tsx:61
|
#: apps/remix/app/(signing)/sign/[token]/no-longer-available.tsx:61
|
||||||
msgid "Want to send slick signing links like this one? <0>Check out Documenso.</0>"
|
msgid "Want to send slick signing links like this one? <0>Check out Documenso.</0>"
|
||||||
msgstr "Möchten Sie auffällige Signatur-Links wie diesen senden? <0>Überprüfen Sie Documenso.</0>"
|
msgstr "Möchten Sie auffällige Signatur-Links wie diesen senden? <0>Überprüfen Sie Documenso.</0>"
|
||||||
```
|
```
|
||||||
|
|||||||
@ -54,7 +54,7 @@ Install the project dependencies as follows:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm i
|
npm i
|
||||||
npm run build:web
|
npm run build
|
||||||
npm run prisma:migrate-deploy
|
npm run prisma:migrate-deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ npm run start
|
|||||||
This will start the server on `localhost:3000`. Any reverse proxy can handle the front end and SSL termination.
|
This will start the server on `localhost:3000`. Any reverse proxy can handle the front end and SSL termination.
|
||||||
|
|
||||||
<Callout type="info">
|
<Callout type="info">
|
||||||
If you want to run with another port than `3000`, you can start the application with `next -p <ANY PORT>` from the `apps/web` folder.
|
If you want to run with another port than `3000`, you can start the application with `next -p <ANY PORT>` from the `apps/remix` folder.
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
</Steps>
|
</Steps>
|
||||||
@ -249,7 +249,7 @@ After=network.target
|
|||||||
Environment=PATH=/path/to/your/node/binaries
|
Environment=PATH=/path/to/your/node/binaries
|
||||||
Type=simple
|
Type=simple
|
||||||
User=www-data
|
User=www-data
|
||||||
WorkingDirectory=/var/www/documenso/apps/web
|
WorkingDirectory=/var/www/documenso/apps/remix
|
||||||
ExecStart=/usr/bin/next start -p 3500
|
ExecStart=/usr/bin/next start -p 3500
|
||||||
TimeoutSec=15
|
TimeoutSec=15
|
||||||
Restart=always
|
Restart=always
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export const AdminDocumentDeleteDialog = ({ document }: AdminDocumentDeleteDialo
|
|||||||
const [reason, setReason] = useState('');
|
const [reason, setReason] = useState('');
|
||||||
|
|
||||||
const { mutateAsync: deleteDocument, isPending: isDeletingDocument } =
|
const { mutateAsync: deleteDocument, isPending: isDeletingDocument } =
|
||||||
trpc.admin.deleteDocument.useMutation();
|
trpc.admin.document.delete.useMutation();
|
||||||
|
|
||||||
const handleDeleteDocument = async () => {
|
const handleDeleteDocument = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -3,12 +3,12 @@ import { useState } from 'react';
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { User } from '@prisma/client';
|
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import type { TGetUserResponse } from '@documenso/trpc/server/admin-router/get-user.types';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -25,7 +25,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
export type AdminUserDeleteDialogProps = {
|
export type AdminUserDeleteDialogProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
user: User;
|
user: TGetUserResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AdminUserDeleteDialog = ({ className, user }: AdminUserDeleteDialogProps) => {
|
export const AdminUserDeleteDialog = ({ className, user }: AdminUserDeleteDialogProps) => {
|
||||||
@ -35,7 +35,7 @@ export const AdminUserDeleteDialog = ({ className, user }: AdminUserDeleteDialog
|
|||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
|
||||||
const { mutateAsync: deleteUser, isPending: isDeletingUser } =
|
const { mutateAsync: deleteUser, isPending: isDeletingUser } =
|
||||||
trpc.admin.deleteUser.useMutation();
|
trpc.admin.user.delete.useMutation();
|
||||||
|
|
||||||
const onDeleteAccount = async () => {
|
const onDeleteAccount = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -3,11 +3,11 @@ import { useState } from 'react';
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { User } from '@prisma/client';
|
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import type { TGetUserResponse } from '@documenso/trpc/server/admin-router/get-user.types';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -24,7 +24,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
export type AdminUserDisableDialogProps = {
|
export type AdminUserDisableDialogProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
userToDisable: User;
|
userToDisable: TGetUserResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AdminUserDisableDialog = ({
|
export const AdminUserDisableDialog = ({
|
||||||
@ -37,7 +37,7 @@ export const AdminUserDisableDialog = ({
|
|||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
|
||||||
const { mutateAsync: disableUser, isPending: isDisablingUser } =
|
const { mutateAsync: disableUser, isPending: isDisablingUser } =
|
||||||
trpc.admin.disableUser.useMutation();
|
trpc.admin.user.disable.useMutation();
|
||||||
|
|
||||||
const onDisableAccount = async () => {
|
const onDisableAccount = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -3,11 +3,11 @@ import { useState } from 'react';
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { User } from '@prisma/client';
|
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import type { TGetUserResponse } from '@documenso/trpc/server/admin-router/get-user.types';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -24,7 +24,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
export type AdminUserEnableDialogProps = {
|
export type AdminUserEnableDialogProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
userToEnable: User;
|
userToEnable: TGetUserResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AdminUserEnableDialog = ({ className, userToEnable }: AdminUserEnableDialogProps) => {
|
export const AdminUserEnableDialog = ({ className, userToEnable }: AdminUserEnableDialogProps) => {
|
||||||
@ -34,7 +34,7 @@ export const AdminUserEnableDialog = ({ className, userToEnable }: AdminUserEnab
|
|||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
|
||||||
const { mutateAsync: enableUser, isPending: isEnablingUser } =
|
const { mutateAsync: enableUser, isPending: isEnablingUser } =
|
||||||
trpc.admin.enableUser.useMutation();
|
trpc.admin.user.enable.useMutation();
|
||||||
|
|
||||||
const onEnableAccount = async () => {
|
const onEnableAccount = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -3,12 +3,12 @@ import { useState } from 'react';
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { User } from '@prisma/client';
|
|
||||||
import { useRevalidator } from 'react-router';
|
import { useRevalidator } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import type { TGetUserResponse } from '@documenso/trpc/server/admin-router/get-user.types';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -25,7 +25,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
export type AdminUserResetTwoFactorDialogProps = {
|
export type AdminUserResetTwoFactorDialogProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
user: User;
|
user: TGetUserResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AdminUserResetTwoFactorDialog = ({
|
export const AdminUserResetTwoFactorDialog = ({
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export const DocumentDeleteDialog = ({
|
|||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
|
const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
|
||||||
|
|
||||||
const { mutateAsync: deleteDocument, isPending } = trpcReact.document.deleteDocument.useMutation({
|
const { mutateAsync: deleteDocument, isPending } = trpcReact.document.delete.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
void refreshLimits();
|
void refreshLimits();
|
||||||
|
|
||||||
|
|||||||
@ -36,11 +36,12 @@ export const DocumentDuplicateDialog = ({
|
|||||||
|
|
||||||
const team = useCurrentTeam();
|
const team = useCurrentTeam();
|
||||||
|
|
||||||
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery(
|
const { data: document, isLoading } = trpcReact.document.get.useQuery(
|
||||||
{
|
{
|
||||||
documentId: id,
|
documentId: id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
queryHash: `document-duplicate-dialog-${id}`,
|
||||||
enabled: open === true,
|
enabled: open === true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -55,7 +56,7 @@ export const DocumentDuplicateDialog = ({
|
|||||||
const documentsPath = formatDocumentsPath(team.url);
|
const documentsPath = formatDocumentsPath(team.url);
|
||||||
|
|
||||||
const { mutateAsync: duplicateDocument, isPending: isDuplicateLoading } =
|
const { mutateAsync: duplicateDocument, isPending: isDuplicateLoading } =
|
||||||
trpcReact.document.duplicateDocument.useMutation({
|
trpcReact.document.duplicate.useMutation({
|
||||||
onSuccess: async ({ documentId }) => {
|
onSuccess: async ({ documentId }) => {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Document Duplicated`),
|
title: _(msg`Document Duplicated`),
|
||||||
|
|||||||
@ -71,7 +71,7 @@ export const DocumentResendDialog = ({ document, recipients }: DocumentResendDia
|
|||||||
document.status !== 'PENDING' ||
|
document.status !== 'PENDING' ||
|
||||||
!recipients.some((r) => r.signingStatus === SigningStatus.NOT_SIGNED);
|
!recipients.some((r) => r.signingStatus === SigningStatus.NOT_SIGNED);
|
||||||
|
|
||||||
const { mutateAsync: resendDocument } = trpcReact.document.resendDocument.useMutation();
|
const { mutateAsync: resendDocument } = trpcReact.document.redistribute.useMutation();
|
||||||
|
|
||||||
const form = useForm<TResendDocumentFormSchema>({
|
const form = useForm<TResendDocumentFormSchema>({
|
||||||
resolver: zodResolver(ZResendDocumentFormSchema),
|
resolver: zodResolver(ZResendDocumentFormSchema),
|
||||||
|
|||||||
@ -65,9 +65,9 @@ export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCre
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: createPasskeyRegistrationOptions, isPending } =
|
const { mutateAsync: createPasskeyRegistrationOptions, isPending } =
|
||||||
trpc.auth.createPasskeyRegistrationOptions.useMutation();
|
trpc.auth.passkey.createRegistrationOptions.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: createPasskey } = trpc.auth.createPasskey.useMutation();
|
const { mutateAsync: createPasskey } = trpc.auth.passkey.create.useMutation();
|
||||||
|
|
||||||
const onFormSubmit = async ({ passkeyName }: TCreatePasskeyFormSchema) => {
|
const onFormSubmit = async ({ passkeyName }: TCreatePasskeyFormSchema) => {
|
||||||
setFormError(null);
|
setFormError(null);
|
||||||
|
|||||||
@ -56,7 +56,7 @@ export default function TokenDeleteDialog({ token, onDelete, children }: TokenDe
|
|||||||
|
|
||||||
type TDeleteTokenByIdMutationSchema = z.infer<typeof ZTokenDeleteDialogSchema>;
|
type TDeleteTokenByIdMutationSchema = z.infer<typeof ZTokenDeleteDialogSchema>;
|
||||||
|
|
||||||
const { mutateAsync: deleteTokenMutation } = trpc.apiToken.deleteTokenById.useMutation({
|
const { mutateAsync: deleteTokenMutation } = trpc.apiToken.delete.useMutation({
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
onDelete?.();
|
onDelete?.();
|
||||||
},
|
},
|
||||||
|
|||||||
@ -172,6 +172,8 @@ export const ConfigureFieldsView = ({
|
|||||||
name: 'fields',
|
name: 'fields',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const highestPageNumber = Math.max(...localFields.map((field) => field.pageNumber));
|
||||||
|
|
||||||
const onFieldCopy = useCallback(
|
const onFieldCopy = useCallback(
|
||||||
(event?: KeyboardEvent | null, options?: { duplicate?: boolean; duplicateAll?: boolean }) => {
|
(event?: KeyboardEvent | null, options?: { duplicate?: boolean; duplicateAll?: boolean }) => {
|
||||||
const { duplicate = false, duplicateAll = false } = options ?? {};
|
const { duplicate = false, duplicateAll = false } = options ?? {};
|
||||||
@ -540,7 +542,9 @@ export const ConfigureFieldsView = ({
|
|||||||
<div>
|
<div>
|
||||||
<PDFViewer documentData={normalizedDocumentData} />
|
<PDFViewer documentData={normalizedDocumentData} />
|
||||||
|
|
||||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
<ElementVisible
|
||||||
|
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}
|
||||||
|
>
|
||||||
{localFields.map((field, index) => {
|
{localFields.map((field, index) => {
|
||||||
const recipientIndex = recipients.findIndex(
|
const recipientIndex = recipients.findIndex(
|
||||||
(r) => r.id === field.recipientId,
|
(r) => r.id === field.recipientId,
|
||||||
|
|||||||
@ -57,7 +57,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
token,
|
token,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
documentData,
|
documentData,
|
||||||
recipient,
|
recipient: _recipient,
|
||||||
fields,
|
fields,
|
||||||
metadata,
|
metadata,
|
||||||
hidePoweredBy = false,
|
hidePoweredBy = false,
|
||||||
@ -91,8 +91,12 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
localFields.filter((field) => field.inserted),
|
localFields.filter((field) => field.inserted),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const highestPendingPageNumber = Math.max(...pendingFields.map((field) => field.page));
|
||||||
|
|
||||||
const hasSignatureField = localFields.some((field) => field.type === FieldType.SIGNATURE);
|
const hasSignatureField = localFields.some((field) => field.type === FieldType.SIGNATURE);
|
||||||
|
|
||||||
|
const signatureValid = !hasSignatureField || (signature && signature.trim() !== '');
|
||||||
|
|
||||||
const { mutateAsync: createDocumentFromDirectTemplate, isPending: isSubmitting } =
|
const { mutateAsync: createDocumentFromDirectTemplate, isPending: isSubmitting } =
|
||||||
trpc.template.createDocumentFromDirectTemplate.useMutation();
|
trpc.template.createDocumentFromDirectTemplate.useMutation();
|
||||||
|
|
||||||
@ -343,19 +347,34 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
<Trans>Sign document</Trans>
|
<Trans>Sign document</Trans>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
{isExpanded ? (
|
||||||
{isExpanded ? (
|
<Button
|
||||||
<LucideChevronDown
|
variant="outline"
|
||||||
className="text-muted-foreground h-5 w-5"
|
className="h-8 w-8 p-0 md:hidden"
|
||||||
onClick={() => setIsExpanded(false)}
|
onClick={() => setIsExpanded(false)}
|
||||||
/>
|
>
|
||||||
) : (
|
<LucideChevronDown className="text-muted-foreground h-5 w-5" />
|
||||||
<LucideChevronUp
|
</Button>
|
||||||
className="text-muted-foreground h-5 w-5"
|
) : pendingFields.length > 0 ? (
|
||||||
onClick={() => setIsExpanded(true)}
|
<Button
|
||||||
/>
|
variant="outline"
|
||||||
)}
|
className="h-8 w-8 p-0 md:hidden"
|
||||||
</Button>
|
onClick={() => setIsExpanded(true)}
|
||||||
|
>
|
||||||
|
<LucideChevronUp className="text-muted-foreground h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
className="md:hidden"
|
||||||
|
disabled={isThrottled || (hasSignatureField && !signatureValid)}
|
||||||
|
loading={isSubmitting}
|
||||||
|
onClick={() => throttledOnCompleteClick()}
|
||||||
|
>
|
||||||
|
<Trans>Complete</Trans>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -442,7 +461,9 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
<ElementVisible
|
||||||
|
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPendingPageNumber}"]`}
|
||||||
|
>
|
||||||
{showPendingFieldTooltip && pendingFields.length > 0 && (
|
{showPendingFieldTooltip && pendingFields.length > 0 && (
|
||||||
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning">
|
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning">
|
||||||
<Trans>Click to insert field</Trans>
|
<Trans>Click to insert field</Trans>
|
||||||
|
|||||||
@ -50,8 +50,10 @@ export const EmbedDocumentFields = ({
|
|||||||
onSignField,
|
onSignField,
|
||||||
onUnsignField,
|
onUnsignField,
|
||||||
}: EmbedDocumentFieldsProps) => {
|
}: EmbedDocumentFieldsProps) => {
|
||||||
|
const highestPageNumber = Math.max(...fields.map((field) => field.page));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
<ElementVisible target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}>
|
||||||
{fields.map((field) =>
|
{fields.map((field) =>
|
||||||
match(field.type)
|
match(field.type)
|
||||||
.with(FieldType.SIGNATURE, () => (
|
.with(FieldType.SIGNATURE, () => (
|
||||||
|
|||||||
@ -89,7 +89,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const [isNameLocked, setIsNameLocked] = useState(false);
|
const [isNameLocked, setIsNameLocked] = useState(false);
|
||||||
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
||||||
const [showOtherRecipientsCompletedFields, setShowOtherRecipientsCompletedFields] =
|
const [_showOtherRecipientsCompletedFields, setShowOtherRecipientsCompletedFields] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
const [allowDocumentRejection, setAllowDocumentRejection] = useState(false);
|
const [allowDocumentRejection, setAllowDocumentRejection] = useState(false);
|
||||||
@ -106,6 +106,8 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
fields.filter((field) => field.inserted),
|
fields.filter((field) => field.inserted),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const highestPendingPageNumber = Math.max(...pendingFields.map((field) => field.page));
|
||||||
|
|
||||||
const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } =
|
const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } =
|
||||||
trpc.recipient.completeDocumentWithToken.useMutation();
|
trpc.recipient.completeDocumentWithToken.useMutation();
|
||||||
|
|
||||||
@ -116,6 +118,8 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
|
|
||||||
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
|
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
|
||||||
|
|
||||||
|
const signatureValid = !hasSignatureField || (signature && signature.trim() !== '');
|
||||||
|
|
||||||
const assistantSignersId = useId();
|
const assistantSignersId = useId();
|
||||||
|
|
||||||
const onNextFieldClick = () => {
|
const onNextFieldClick = () => {
|
||||||
@ -305,19 +309,36 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
{isExpanded ? (
|
||||||
{isExpanded ? (
|
<Button
|
||||||
<LucideChevronDown
|
variant="outline"
|
||||||
className="text-muted-foreground h-5 w-5"
|
className="bg-background dark:bg-foreground h-8 w-8 p-0 md:hidden"
|
||||||
onClick={() => setIsExpanded(false)}
|
onClick={() => setIsExpanded(false)}
|
||||||
/>
|
>
|
||||||
) : (
|
<LucideChevronDown className="text-muted-foreground dark:text-background h-5 w-5" />
|
||||||
<LucideChevronUp
|
</Button>
|
||||||
className="text-muted-foreground h-5 w-5"
|
) : pendingFields.length > 0 ? (
|
||||||
onClick={() => setIsExpanded(true)}
|
<Button
|
||||||
/>
|
variant="outline"
|
||||||
)}
|
className="bg-background dark:bg-foreground h-8 w-8 p-0 md:hidden"
|
||||||
</Button>
|
onClick={() => setIsExpanded(true)}
|
||||||
|
>
|
||||||
|
<LucideChevronUp className="text-muted-foreground dark:text-background h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
className="md:hidden"
|
||||||
|
disabled={
|
||||||
|
isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
|
||||||
|
}
|
||||||
|
loading={isSubmitting}
|
||||||
|
onClick={() => throttledOnCompleteClick()}
|
||||||
|
>
|
||||||
|
<Trans>Complete</Trans>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -465,7 +486,9 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
<ElementVisible
|
||||||
|
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPendingPageNumber}"]`}
|
||||||
|
>
|
||||||
{showPendingFieldTooltip && pendingFields.length > 0 && (
|
{showPendingFieldTooltip && pendingFields.length > 0 && (
|
||||||
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning">
|
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning">
|
||||||
<Trans>Click to insert field</Trans>
|
<Trans>Click to insert field</Trans>
|
||||||
|
|||||||
@ -92,6 +92,8 @@ export const MultiSignDocumentSigningView = ({
|
|||||||
[],
|
[],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const highestPendingPageNumber = Math.max(...pendingFields.map((field) => field.page));
|
||||||
|
|
||||||
const uninsertedFields = document?.fields.filter((field) => !field.inserted) ?? [];
|
const uninsertedFields = document?.fields.filter((field) => !field.inserted) ?? [];
|
||||||
|
|
||||||
const onSignField = async (payload: TSignFieldWithTokenMutationSchema) => {
|
const onSignField = async (payload: TSignFieldWithTokenMutationSchema) => {
|
||||||
@ -357,7 +359,9 @@ export const MultiSignDocumentSigningView = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasDocumentLoaded && (
|
{hasDocumentLoaded && (
|
||||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
<ElementVisible
|
||||||
|
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPendingPageNumber}"]`}
|
||||||
|
>
|
||||||
{showPendingFieldTooltip && pendingFields.length > 0 && (
|
{showPendingFieldTooltip && pendingFields.length > 0 && (
|
||||||
<FieldToolTip
|
<FieldToolTip
|
||||||
key={pendingFields[0].id}
|
key={pendingFields[0].id}
|
||||||
|
|||||||
@ -114,7 +114,7 @@ export const SignInForm = ({
|
|||||||
}, [returnTo]);
|
}, [returnTo]);
|
||||||
|
|
||||||
const { mutateAsync: createPasskeySigninOptions } =
|
const { mutateAsync: createPasskeySigninOptions } =
|
||||||
trpc.auth.createPasskeySigninOptions.useMutation();
|
trpc.auth.passkey.createSigninOptions.useMutation();
|
||||||
|
|
||||||
const form = useForm<TSignInFormSchema>({
|
const form = useForm<TSignInFormSchema>({
|
||||||
values: {
|
values: {
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import type { z } from 'zod';
|
|||||||
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { ZCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
|
import { ZCreateApiTokenRequestSchema } from '@documenso/trpc/server/api-token-router/create-api-token.types';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
@ -47,7 +47,7 @@ export const EXPIRATION_DATES = {
|
|||||||
ONE_YEAR: msg`12 months`,
|
ONE_YEAR: msg`12 months`,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const ZCreateTokenFormSchema = ZCreateTokenMutationSchema.pick({
|
const ZCreateTokenFormSchema = ZCreateApiTokenRequestSchema.pick({
|
||||||
tokenName: true,
|
tokenName: true,
|
||||||
expirationDate: true,
|
expirationDate: true,
|
||||||
});
|
});
|
||||||
@ -75,7 +75,7 @@ export const ApiTokenForm = ({ className, tokens }: ApiTokenFormProps) => {
|
|||||||
const [newlyCreatedToken, setNewlyCreatedToken] = useState<NewlyCreatedToken | null>();
|
const [newlyCreatedToken, setNewlyCreatedToken] = useState<NewlyCreatedToken | null>();
|
||||||
const [noExpirationDate, setNoExpirationDate] = useState(false);
|
const [noExpirationDate, setNoExpirationDate] = useState(false);
|
||||||
|
|
||||||
const { mutateAsync: createTokenMutation } = trpc.apiToken.createToken.useMutation({
|
const { mutateAsync: createTokenMutation } = trpc.apiToken.create.useMutation({
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
setNewlyCreatedToken(data);
|
setNewlyCreatedToken(data);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import type { TooltipProps } from 'recharts';
|
import type { TooltipProps } from 'recharts';
|
||||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||||
|
|||||||
@ -64,7 +64,7 @@ export function AppCommandMenu({ open, onOpenChange }: AppCommandMenuProps) {
|
|||||||
const [pages, setPages] = useState<string[]>([]);
|
const [pages, setPages] = useState<string[]>([]);
|
||||||
|
|
||||||
const { data: searchDocumentsData, isPending: isSearchingDocuments } =
|
const { data: searchDocumentsData, isPending: isSearchingDocuments } =
|
||||||
trpcReact.document.searchDocuments.useQuery(
|
trpcReact.document.search.useQuery(
|
||||||
{
|
{
|
||||||
query: search,
|
query: search,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -79,6 +79,8 @@ export const DirectTemplateSigningForm = ({
|
|||||||
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const highestPageNumber = Math.max(...localFields.map((field) => field.page));
|
||||||
|
|
||||||
const fieldsRequiringValidation = useMemo(() => {
|
const fieldsRequiringValidation = useMemo(() => {
|
||||||
return localFields.filter((field) => isFieldUnsignedAndRequired(field));
|
return localFields.filter((field) => isFieldUnsignedAndRequired(field));
|
||||||
}, [localFields]);
|
}, [localFields]);
|
||||||
@ -221,7 +223,9 @@ export const DirectTemplateSigningForm = ({
|
|||||||
<DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} />
|
<DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} />
|
||||||
|
|
||||||
<DocumentFlowFormContainerContent>
|
<DocumentFlowFormContainerContent>
|
||||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
<ElementVisible
|
||||||
|
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}
|
||||||
|
>
|
||||||
{validateUninsertedFields && uninsertedFields[0] && (
|
{validateUninsertedFields && uninsertedFields[0] && (
|
||||||
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
|
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
|
||||||
<Trans>Click to insert field</Trans>
|
<Trans>Click to insert field</Trans>
|
||||||
|
|||||||
@ -77,7 +77,7 @@ export const DocumentSigningAuthPasskey = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: createPasskeyAuthenticationOptions } =
|
const { mutateAsync: createPasskeyAuthenticationOptions } =
|
||||||
trpc.auth.createPasskeyAuthenticationOptions.useMutation();
|
trpc.auth.passkey.createAuthenticationOptions.useMutation();
|
||||||
|
|
||||||
const [formErrorCode, setFormErrorCode] = useState<string | null>(null);
|
const [formErrorCode, setFormErrorCode] = useState<string | null>(null);
|
||||||
|
|
||||||
|
|||||||
@ -93,7 +93,7 @@ export const DocumentSigningAuthProvider = ({
|
|||||||
[documentAuthOptions, recipient],
|
[documentAuthOptions, recipient],
|
||||||
);
|
);
|
||||||
|
|
||||||
const passkeyQuery = trpc.auth.findPasskeys.useQuery(
|
const passkeyQuery = trpc.auth.passkey.find.useQuery(
|
||||||
{
|
{
|
||||||
perPage: MAXIMUM_PASSKEYS,
|
perPage: MAXIMUM_PASSKEYS,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -7,14 +7,11 @@ import { type Field, FieldType, type Recipient, RecipientRole } from '@prisma/cl
|
|||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
|
||||||
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
|
||||||
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
|
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
import { sortFieldsByPosition } from '@documenso/lib/utils/fields';
|
||||||
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
@ -34,29 +31,33 @@ export type DocumentSigningFormProps = {
|
|||||||
document: DocumentAndSender;
|
document: DocumentAndSender;
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
redirectUrl?: string | null;
|
|
||||||
isRecipientsTurn: boolean;
|
isRecipientsTurn: boolean;
|
||||||
allRecipients?: RecipientWithFields[];
|
allRecipients?: RecipientWithFields[];
|
||||||
setSelectedSignerId?: (id: number | null) => void;
|
setSelectedSignerId?: (id: number | null) => void;
|
||||||
|
completeDocument: (
|
||||||
|
authOptions?: TRecipientActionAuth,
|
||||||
|
nextSigner?: { email: string; name: string },
|
||||||
|
) => Promise<void>;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
fieldsValidated: () => void;
|
||||||
|
nextRecipient?: RecipientWithFields;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentSigningForm = ({
|
export const DocumentSigningForm = ({
|
||||||
document,
|
document,
|
||||||
recipient,
|
recipient,
|
||||||
fields,
|
fields,
|
||||||
redirectUrl,
|
|
||||||
isRecipientsTurn,
|
isRecipientsTurn,
|
||||||
allRecipients = [],
|
allRecipients = [],
|
||||||
setSelectedSignerId,
|
setSelectedSignerId,
|
||||||
|
completeDocument,
|
||||||
|
isSubmitting,
|
||||||
|
fieldsValidated,
|
||||||
|
nextRecipient,
|
||||||
}: DocumentSigningFormProps) => {
|
}: DocumentSigningFormProps) => {
|
||||||
const { sessionData } = useOptionalSession();
|
|
||||||
const user = sessionData?.user;
|
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const analytics = useAnalytics();
|
|
||||||
|
|
||||||
const assistantSignersId = useId();
|
const assistantSignersId = useId();
|
||||||
|
|
||||||
@ -66,21 +67,12 @@ export const DocumentSigningForm = ({
|
|||||||
const [isConfirmationDialogOpen, setIsConfirmationDialogOpen] = useState(false);
|
const [isConfirmationDialogOpen, setIsConfirmationDialogOpen] = useState(false);
|
||||||
const [isAssistantSubmitting, setIsAssistantSubmitting] = useState(false);
|
const [isAssistantSubmitting, setIsAssistantSubmitting] = useState(false);
|
||||||
|
|
||||||
const {
|
|
||||||
mutateAsync: completeDocumentWithToken,
|
|
||||||
isPending,
|
|
||||||
isSuccess,
|
|
||||||
} = trpc.recipient.completeDocumentWithToken.useMutation();
|
|
||||||
|
|
||||||
const assistantForm = useForm<{ selectedSignerId: number | undefined }>({
|
const assistantForm = useForm<{ selectedSignerId: number | undefined }>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
selectedSignerId: undefined,
|
selectedSignerId: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Keep the loading state going if successful since the redirect may take some time.
|
|
||||||
const isSubmitting = isPending || isSuccess;
|
|
||||||
|
|
||||||
const fieldsRequiringValidation = useMemo(
|
const fieldsRequiringValidation = useMemo(
|
||||||
() => fields.filter(isFieldUnsignedAndRequired),
|
() => fields.filter(isFieldUnsignedAndRequired),
|
||||||
[fields],
|
[fields],
|
||||||
@ -96,9 +88,9 @@ export const DocumentSigningForm = ({
|
|||||||
return fieldsRequiringValidation.filter((field) => field.recipientId === recipient.id);
|
return fieldsRequiringValidation.filter((field) => field.recipientId === recipient.id);
|
||||||
}, [fieldsRequiringValidation, recipient]);
|
}, [fieldsRequiringValidation, recipient]);
|
||||||
|
|
||||||
const fieldsValidated = () => {
|
const localFieldsValidated = () => {
|
||||||
setValidateUninsertedFields(true);
|
setValidateUninsertedFields(true);
|
||||||
validateFieldsInserted(fieldsRequiringValidation);
|
fieldsValidated();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAssistantFormSubmit = () => {
|
const onAssistantFormSubmit = () => {
|
||||||
@ -126,55 +118,6 @@ export const DocumentSigningForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const completeDocument = async (
|
|
||||||
authOptions?: TRecipientActionAuth,
|
|
||||||
nextSigner?: { email: string; name: string },
|
|
||||||
) => {
|
|
||||||
const payload = {
|
|
||||||
token: recipient.token,
|
|
||||||
documentId: document.id,
|
|
||||||
authOptions,
|
|
||||||
...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
await completeDocumentWithToken(payload);
|
|
||||||
|
|
||||||
analytics.capture('App: Recipient has completed signing', {
|
|
||||||
signerId: recipient.id,
|
|
||||||
documentId: document.id,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (redirectUrl) {
|
|
||||||
window.location.href = redirectUrl;
|
|
||||||
} else {
|
|
||||||
await navigate(`/sign/${recipient.token}/complete`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const nextRecipient = useMemo(() => {
|
|
||||||
if (
|
|
||||||
!document.documentMeta?.signingOrder ||
|
|
||||||
document.documentMeta.signingOrder !== 'SEQUENTIAL'
|
|
||||||
) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedRecipients = allRecipients.sort((a, b) => {
|
|
||||||
// Sort by signingOrder first (nulls last), then by id
|
|
||||||
if (a.signingOrder === null && b.signingOrder === null) return a.id - b.id;
|
|
||||||
if (a.signingOrder === null) return 1;
|
|
||||||
if (b.signingOrder === null) return -1;
|
|
||||||
if (a.signingOrder === b.signingOrder) return a.id - b.id;
|
|
||||||
return a.signingOrder - b.signingOrder;
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentIndex = sortedRecipients.findIndex((r) => r.id === recipient.id);
|
|
||||||
return currentIndex !== -1 && currentIndex < sortedRecipients.length - 1
|
|
||||||
? sortedRecipients[currentIndex + 1]
|
|
||||||
: undefined;
|
|
||||||
}, [document.documentMeta?.signingOrder, allRecipients, recipient.id]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
{validateUninsertedFields && uninsertedFields[0] && (
|
{validateUninsertedFields && uninsertedFields[0] && (
|
||||||
@ -205,7 +148,7 @@ export const DocumentSigningForm = ({
|
|||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
documentTitle={document.title}
|
documentTitle={document.title}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
fieldsValidated={fieldsValidated}
|
fieldsValidated={localFieldsValidated}
|
||||||
onSignatureComplete={async (nextSigner) => {
|
onSignatureComplete={async (nextSigner) => {
|
||||||
await completeDocument(undefined, nextSigner);
|
await completeDocument(undefined, nextSigner);
|
||||||
}}
|
}}
|
||||||
@ -364,7 +307,7 @@ export const DocumentSigningForm = ({
|
|||||||
isSubmitting={isSubmitting || isAssistantSubmitting}
|
isSubmitting={isSubmitting || isAssistantSubmitting}
|
||||||
documentTitle={document.title}
|
documentTitle={document.title}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
fieldsValidated={fieldsValidated}
|
fieldsValidated={localFieldsValidated}
|
||||||
disabled={!isRecipientsTurn}
|
disabled={!isRecipientsTurn}
|
||||||
onSignatureComplete={async (nextSigner) => {
|
onSignatureComplete={async (nextSigner) => {
|
||||||
await completeDocument(undefined, nextSigner);
|
await completeDocument(undefined, nextSigner);
|
||||||
|
|||||||
@ -1,15 +1,18 @@
|
|||||||
import { useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { Field } from '@prisma/client';
|
import type { Field } from '@prisma/client';
|
||||||
import { FieldType, RecipientRole } from '@prisma/client';
|
import { FieldType, RecipientRole } from '@prisma/client';
|
||||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||||
import { match } from 'ts-pattern';
|
import { useNavigate } from 'react-router';
|
||||||
|
import { P, match } from 'ts-pattern';
|
||||||
|
|
||||||
|
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||||
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
|
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
|
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import {
|
import {
|
||||||
ZCheckboxFieldMeta,
|
ZCheckboxFieldMeta,
|
||||||
ZDropdownFieldMeta,
|
ZDropdownFieldMeta,
|
||||||
@ -18,8 +21,11 @@ import {
|
|||||||
ZTextFieldMeta,
|
ZTextFieldMeta,
|
||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
import type { CompletedField } from '@documenso/lib/types/fields';
|
import type { CompletedField } from '@documenso/lib/types/fields';
|
||||||
|
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||||
|
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
||||||
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
|
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
@ -40,6 +46,7 @@ import { DocumentSigningRejectDialog } from '~/components/general/document-signi
|
|||||||
import { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-field';
|
import { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-field';
|
||||||
import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
|
import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
|
||||||
|
|
||||||
|
import { DocumentSigningCompleteDialog } from './document-signing-complete-dialog';
|
||||||
import { DocumentSigningRecipientProvider } from './document-signing-recipient-provider';
|
import { DocumentSigningRecipientProvider } from './document-signing-recipient-provider';
|
||||||
|
|
||||||
export type DocumentSigningPageViewProps = {
|
export type DocumentSigningPageViewProps = {
|
||||||
@ -63,9 +70,56 @@ export const DocumentSigningPageView = ({
|
|||||||
}: DocumentSigningPageViewProps) => {
|
}: DocumentSigningPageViewProps) => {
|
||||||
const { documentData, documentMeta } = document;
|
const { documentData, documentMeta } = document;
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const analytics = useAnalytics();
|
||||||
|
|
||||||
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(allRecipients?.[0]?.id);
|
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(allRecipients?.[0]?.id);
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutateAsync: completeDocumentWithToken,
|
||||||
|
isPending,
|
||||||
|
isSuccess,
|
||||||
|
} = trpc.recipient.completeDocumentWithToken.useMutation();
|
||||||
|
|
||||||
|
// Keep the loading state going if successful since the redirect may take some time.
|
||||||
|
const isSubmitting = isPending || isSuccess;
|
||||||
|
|
||||||
|
const fieldsRequiringValidation = useMemo(
|
||||||
|
() => fields.filter(isFieldUnsignedAndRequired),
|
||||||
|
[fields],
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldsValidated = () => {
|
||||||
|
validateFieldsInserted(fieldsRequiringValidation);
|
||||||
|
};
|
||||||
|
|
||||||
|
const completeDocument = async (
|
||||||
|
authOptions?: TRecipientActionAuth,
|
||||||
|
nextSigner?: { email: string; name: string },
|
||||||
|
) => {
|
||||||
|
const payload = {
|
||||||
|
token: recipient.token,
|
||||||
|
documentId: document.id,
|
||||||
|
authOptions,
|
||||||
|
...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
await completeDocumentWithToken(payload);
|
||||||
|
|
||||||
|
analytics.capture('App: Recipient has completed signing', {
|
||||||
|
signerId: recipient.id,
|
||||||
|
documentId: document.id,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (documentMeta?.redirectUrl) {
|
||||||
|
window.location.href = documentMeta.redirectUrl;
|
||||||
|
} else {
|
||||||
|
await navigate(`/sign/${recipient.token}/complete`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let senderName = document.user.name ?? '';
|
let senderName = document.user.name ?? '';
|
||||||
let senderEmail = `(${document.user.email})`;
|
let senderEmail = `(${document.user.email})`;
|
||||||
|
|
||||||
@ -78,6 +132,31 @@ export const DocumentSigningPageView = ({
|
|||||||
const targetSigner =
|
const targetSigner =
|
||||||
recipient.role === RecipientRole.ASSISTANT && selectedSigner ? selectedSigner : null;
|
recipient.role === RecipientRole.ASSISTANT && selectedSigner ? selectedSigner : null;
|
||||||
|
|
||||||
|
const nextRecipient = useMemo(() => {
|
||||||
|
if (!documentMeta?.signingOrder || documentMeta.signingOrder !== 'SEQUENTIAL') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedRecipients = allRecipients.sort((a, b) => {
|
||||||
|
// Sort by signingOrder first (nulls last), then by id
|
||||||
|
if (a.signingOrder === null && b.signingOrder === null) return a.id - b.id;
|
||||||
|
if (a.signingOrder === null) return 1;
|
||||||
|
if (b.signingOrder === null) return -1;
|
||||||
|
if (a.signingOrder === b.signingOrder) return a.id - b.id;
|
||||||
|
return a.signingOrder - b.signingOrder;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentIndex = sortedRecipients.findIndex((r) => r.id === recipient.id);
|
||||||
|
return currentIndex !== -1 && currentIndex < sortedRecipients.length - 1
|
||||||
|
? sortedRecipients[currentIndex + 1]
|
||||||
|
: undefined;
|
||||||
|
}, [document.documentMeta?.signingOrder, allRecipients, recipient.id]);
|
||||||
|
|
||||||
|
const highestPageNumber = Math.max(...fields.map((field) => field.page));
|
||||||
|
|
||||||
|
const pendingFields = fieldsRequiringValidation.filter((field) => !field.inserted);
|
||||||
|
const hasPendingFields = pendingFields.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentSigningRecipientProvider recipient={recipient} targetSigner={targetSigner}>
|
<DocumentSigningRecipientProvider recipient={recipient} targetSigner={targetSigner}>
|
||||||
<div className="mx-auto w-full max-w-screen-xl sm:px-6">
|
<div className="mx-auto w-full max-w-screen-xl sm:px-6">
|
||||||
@ -163,19 +242,55 @@ export const DocumentSigningPageView = ({
|
|||||||
.otherwise(() => null)}
|
.otherwise(() => null)}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
{match({ hasPendingFields, isExpanded, role: recipient.role })
|
||||||
{isExpanded ? (
|
.with(
|
||||||
<LucideChevronDown
|
{
|
||||||
className="text-muted-foreground h-5 w-5"
|
hasPendingFields: false,
|
||||||
|
role: P.not(RecipientRole.ASSISTANT),
|
||||||
|
isExpanded: false,
|
||||||
|
},
|
||||||
|
() => (
|
||||||
|
<div className="md:hidden">
|
||||||
|
<DocumentSigningCompleteDialog
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
documentTitle={document.title}
|
||||||
|
fields={fields}
|
||||||
|
fieldsValidated={fieldsValidated}
|
||||||
|
disabled={!isRecipientsTurn}
|
||||||
|
onSignatureComplete={async (nextSigner) => {
|
||||||
|
await completeDocument(undefined, nextSigner);
|
||||||
|
}}
|
||||||
|
role={recipient.role}
|
||||||
|
allowDictateNextSigner={
|
||||||
|
nextRecipient && documentMeta?.allowDictateNextSigner
|
||||||
|
}
|
||||||
|
defaultNextSigner={
|
||||||
|
nextRecipient
|
||||||
|
? { name: nextRecipient.name, email: nextRecipient.email }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with({ isExpanded: true }, () => (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="bg-background dark:bg-foreground h-8 w-8 p-0 md:hidden"
|
||||||
onClick={() => setIsExpanded(false)}
|
onClick={() => setIsExpanded(false)}
|
||||||
/>
|
>
|
||||||
) : (
|
<LucideChevronDown className="text-muted-foreground dark:text-background h-5 w-5" />
|
||||||
<LucideChevronUp
|
</Button>
|
||||||
className="text-muted-foreground h-5 w-5"
|
))
|
||||||
|
.otherwise(() => (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="bg-background dark:bg-foreground h-8 w-8 p-0 md:hidden"
|
||||||
onClick={() => setIsExpanded(true)}
|
onClick={() => setIsExpanded(true)}
|
||||||
/>
|
>
|
||||||
)}
|
<LucideChevronUp className="text-muted-foreground dark:text-background h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden group-data-[expanded]/document-widget:block md:block">
|
<div className="hidden group-data-[expanded]/document-widget:block md:block">
|
||||||
@ -204,10 +319,13 @@ export const DocumentSigningPageView = ({
|
|||||||
document={document}
|
document={document}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
redirectUrl={documentMeta?.redirectUrl}
|
|
||||||
isRecipientsTurn={isRecipientsTurn}
|
isRecipientsTurn={isRecipientsTurn}
|
||||||
allRecipients={allRecipients}
|
allRecipients={allRecipients}
|
||||||
setSelectedSignerId={setSelectedSignerId}
|
setSelectedSignerId={setSelectedSignerId}
|
||||||
|
completeDocument={completeDocument}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
fieldsValidated={fieldsValidated}
|
||||||
|
nextRecipient={nextRecipient}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -224,7 +342,9 @@ export const DocumentSigningPageView = ({
|
|||||||
<DocumentSigningAutoSign recipient={recipient} fields={fields} />
|
<DocumentSigningAutoSign recipient={recipient} fields={fields} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
<ElementVisible
|
||||||
|
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}
|
||||||
|
>
|
||||||
{fields
|
{fields
|
||||||
.filter(
|
.filter(
|
||||||
(field) =>
|
(field) =>
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export const DocumentAuditLogDownloadButton = ({
|
|||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const { mutateAsync: downloadAuditLogs, isPending } =
|
const { mutateAsync: downloadAuditLogs, isPending } =
|
||||||
trpc.document.downloadAuditLogs.useMutation();
|
trpc.document.auditLog.download.useMutation();
|
||||||
|
|
||||||
const onDownloadAuditLogsClick = async () => {
|
const onDownloadAuditLogsClick = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZon
|
|||||||
|
|
||||||
const { quota, remaining, refreshLimits } = useLimits();
|
const { quota, remaining, refreshLimits } = useLimits();
|
||||||
|
|
||||||
const { mutateAsync: createDocument } = trpc.document.createDocument.useMutation();
|
const { mutateAsync: createDocument } = trpc.document.create.useMutation();
|
||||||
|
|
||||||
const isUploadDisabled = remaining.documents === 0 || !user.emailVerified;
|
const isUploadDisabled = remaining.documents === 0 || !user.emailVerified;
|
||||||
|
|
||||||
|
|||||||
@ -59,23 +59,22 @@ export const DocumentEditForm = ({
|
|||||||
|
|
||||||
const utils = trpc.useUtils();
|
const utils = trpc.useUtils();
|
||||||
|
|
||||||
const { data: document, refetch: refetchDocument } =
|
const { data: document, refetch: refetchDocument } = trpc.document.get.useQuery(
|
||||||
trpc.document.getDocumentWithDetailsById.useQuery(
|
{
|
||||||
{
|
documentId: initialDocument.id,
|
||||||
documentId: initialDocument.id,
|
},
|
||||||
},
|
{
|
||||||
{
|
initialData: initialDocument,
|
||||||
initialData: initialDocument,
|
...SKIP_QUERY_BATCH_META,
|
||||||
...SKIP_QUERY_BATCH_META,
|
},
|
||||||
},
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const { recipients, fields } = document;
|
const { recipients, fields } = document;
|
||||||
|
|
||||||
const { mutateAsync: updateDocument } = trpc.document.updateDocument.useMutation({
|
const { mutateAsync: updateDocument } = trpc.document.update.useMutation({
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
onSuccess: (newData) => {
|
onSuccess: (newData) => {
|
||||||
utils.document.getDocumentWithDetailsById.setData(
|
utils.document.get.setData(
|
||||||
{
|
{
|
||||||
documentId: initialDocument.id,
|
documentId: initialDocument.id,
|
||||||
},
|
},
|
||||||
@ -84,23 +83,10 @@ export const DocumentEditForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: setSigningOrderForDocument } =
|
|
||||||
trpc.document.setSigningOrderForDocument.useMutation({
|
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
|
||||||
onSuccess: (newData) => {
|
|
||||||
utils.document.getDocumentWithDetailsById.setData(
|
|
||||||
{
|
|
||||||
documentId: initialDocument.id,
|
|
||||||
},
|
|
||||||
(oldData) => ({ ...(oldData || initialDocument), ...newData, id: Number(newData.id) }),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutateAsync: addFields } = trpc.field.addFields.useMutation({
|
const { mutateAsync: addFields } = trpc.field.addFields.useMutation({
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
onSuccess: ({ fields: newFields }) => {
|
onSuccess: ({ fields: newFields }) => {
|
||||||
utils.document.getDocumentWithDetailsById.setData(
|
utils.document.get.setData(
|
||||||
{
|
{
|
||||||
documentId: initialDocument.id,
|
documentId: initialDocument.id,
|
||||||
},
|
},
|
||||||
@ -112,7 +98,7 @@ export const DocumentEditForm = ({
|
|||||||
const { mutateAsync: setRecipients } = trpc.recipient.setDocumentRecipients.useMutation({
|
const { mutateAsync: setRecipients } = trpc.recipient.setDocumentRecipients.useMutation({
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
onSuccess: ({ recipients: newRecipients }) => {
|
onSuccess: ({ recipients: newRecipients }) => {
|
||||||
utils.document.getDocumentWithDetailsById.setData(
|
utils.document.get.setData(
|
||||||
{
|
{
|
||||||
documentId: initialDocument.id,
|
documentId: initialDocument.id,
|
||||||
},
|
},
|
||||||
@ -121,10 +107,10 @@ export const DocumentEditForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: sendDocument } = trpc.document.sendDocument.useMutation({
|
const { mutateAsync: sendDocument } = trpc.document.distribute.useMutation({
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
onSuccess: (newData) => {
|
onSuccess: (newData) => {
|
||||||
utils.document.getDocumentWithDetailsById.setData(
|
utils.document.get.setData(
|
||||||
{
|
{
|
||||||
documentId: initialDocument.id,
|
documentId: initialDocument.id,
|
||||||
},
|
},
|
||||||
@ -173,34 +159,37 @@ export const DocumentEditForm = ({
|
|||||||
return initialStep;
|
return initialStep;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const saveSettingsData = async (data: TAddSettingsFormSchema) => {
|
||||||
|
const { timezone, dateFormat, redirectUrl, language, signatureTypes } = data.meta;
|
||||||
|
|
||||||
|
const parsedGlobalAccessAuth = z
|
||||||
|
.array(ZDocumentAccessAuthTypesSchema)
|
||||||
|
.safeParse(data.globalAccessAuth);
|
||||||
|
|
||||||
|
return updateDocument({
|
||||||
|
documentId: document.id,
|
||||||
|
data: {
|
||||||
|
title: data.title,
|
||||||
|
externalId: data.externalId || null,
|
||||||
|
visibility: data.visibility,
|
||||||
|
globalAccessAuth: parsedGlobalAccessAuth.success ? parsedGlobalAccessAuth.data : [],
|
||||||
|
globalActionAuth: data.globalActionAuth ?? [],
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
timezone,
|
||||||
|
dateFormat,
|
||||||
|
redirectUrl,
|
||||||
|
language: isValidLanguageCode(language) ? language : undefined,
|
||||||
|
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
|
||||||
|
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
||||||
|
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const onAddSettingsFormSubmit = async (data: TAddSettingsFormSchema) => {
|
const onAddSettingsFormSubmit = async (data: TAddSettingsFormSchema) => {
|
||||||
try {
|
try {
|
||||||
const { timezone, dateFormat, redirectUrl, language, signatureTypes } = data.meta;
|
await saveSettingsData(data);
|
||||||
|
|
||||||
const parsedGlobalAccessAuth = z
|
|
||||||
.array(ZDocumentAccessAuthTypesSchema)
|
|
||||||
.safeParse(data.globalAccessAuth);
|
|
||||||
|
|
||||||
await updateDocument({
|
|
||||||
documentId: document.id,
|
|
||||||
data: {
|
|
||||||
title: data.title,
|
|
||||||
externalId: data.externalId || null,
|
|
||||||
visibility: data.visibility,
|
|
||||||
globalAccessAuth: parsedGlobalAccessAuth.success ? parsedGlobalAccessAuth.data : [],
|
|
||||||
globalActionAuth: data.globalActionAuth ?? [],
|
|
||||||
},
|
|
||||||
meta: {
|
|
||||||
timezone,
|
|
||||||
dateFormat,
|
|
||||||
redirectUrl,
|
|
||||||
language: isValidLanguageCode(language) ? language : undefined,
|
|
||||||
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
|
|
||||||
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
|
||||||
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setStep('signers');
|
setStep('signers');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -213,30 +202,58 @@ export const DocumentEditForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onAddSettingsFormAutoSave = async (data: TAddSettingsFormSchema) => {
|
||||||
|
try {
|
||||||
|
await saveSettingsData(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: _(msg`Error`),
|
||||||
|
description: _(msg`An error occurred while auto-saving the document settings.`),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveSignersData = async (data: TAddSignersFormSchema) => {
|
||||||
|
return Promise.all([
|
||||||
|
updateDocument({
|
||||||
|
documentId: document.id,
|
||||||
|
meta: {
|
||||||
|
allowDictateNextSigner: data.allowDictateNextSigner,
|
||||||
|
signingOrder: data.signingOrder,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
setRecipients({
|
||||||
|
documentId: document.id,
|
||||||
|
recipients: data.signers.map((signer) => ({
|
||||||
|
...signer,
|
||||||
|
// Explicitly set to null to indicate we want to remove auth if required.
|
||||||
|
actionAuth: signer.actionAuth ?? [],
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddSignersFormAutoSave = async (data: TAddSignersFormSchema) => {
|
||||||
|
try {
|
||||||
|
await saveSignersData(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: _(msg`Error`),
|
||||||
|
description: _(msg`An error occurred while adding signers.`),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => {
|
const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await saveSignersData(data);
|
||||||
setSigningOrderForDocument({
|
|
||||||
documentId: document.id,
|
|
||||||
signingOrder: data.signingOrder,
|
|
||||||
}),
|
|
||||||
|
|
||||||
updateDocument({
|
|
||||||
documentId: document.id,
|
|
||||||
meta: {
|
|
||||||
allowDictateNextSigner: data.allowDictateNextSigner,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
setRecipients({
|
|
||||||
documentId: document.id,
|
|
||||||
recipients: data.signers.map((signer) => ({
|
|
||||||
...signer,
|
|
||||||
// Explicitly set to null to indicate we want to remove auth if required.
|
|
||||||
actionAuth: signer.actionAuth ?? [],
|
|
||||||
})),
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
setStep('fields');
|
setStep('fields');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -250,12 +267,16 @@ export const DocumentEditForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveFieldsData = async (data: TAddFieldsFormSchema) => {
|
||||||
|
return addFields({
|
||||||
|
documentId: document.id,
|
||||||
|
fields: data.fields,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const onAddFieldsFormSubmit = async (data: TAddFieldsFormSchema) => {
|
const onAddFieldsFormSubmit = async (data: TAddFieldsFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await addFields({
|
await saveFieldsData(data);
|
||||||
documentId: document.id,
|
|
||||||
fields: data.fields,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear all field data from localStorage
|
// Clear all field data from localStorage
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
@ -277,24 +298,60 @@ export const DocumentEditForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => {
|
const onAddFieldsFormAutoSave = async (data: TAddFieldsFormSchema) => {
|
||||||
|
try {
|
||||||
|
await saveFieldsData(data);
|
||||||
|
// Don't clear localStorage on auto-save, only on explicit submit
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: _(msg`Error`),
|
||||||
|
description: _(msg`An error occurred while auto-saving the fields.`),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveSubjectData = async (data: TAddSubjectFormSchema) => {
|
||||||
const { subject, message, distributionMethod, emailId, emailReplyTo, emailSettings } =
|
const { subject, message, distributionMethod, emailId, emailReplyTo, emailSettings } =
|
||||||
data.meta;
|
data.meta;
|
||||||
|
|
||||||
try {
|
return updateDocument({
|
||||||
await sendDocument({
|
documentId: document.id,
|
||||||
documentId: document.id,
|
meta: {
|
||||||
meta: {
|
subject,
|
||||||
subject,
|
message,
|
||||||
message,
|
distributionMethod,
|
||||||
distributionMethod,
|
emailId,
|
||||||
emailId,
|
emailReplyTo,
|
||||||
emailReplyTo: emailReplyTo || null,
|
emailSettings: emailSettings,
|
||||||
emailSettings: emailSettings,
|
},
|
||||||
},
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
if (distributionMethod === DocumentDistributionMethod.EMAIL) {
|
const sendDocumentWithSubject = async (data: TAddSubjectFormSchema) => {
|
||||||
|
const { subject, message, distributionMethod, emailId, emailReplyTo, emailSettings } =
|
||||||
|
data.meta;
|
||||||
|
|
||||||
|
return sendDocument({
|
||||||
|
documentId: document.id,
|
||||||
|
meta: {
|
||||||
|
subject,
|
||||||
|
message,
|
||||||
|
distributionMethod,
|
||||||
|
emailId,
|
||||||
|
emailReplyTo: emailReplyTo || null,
|
||||||
|
emailSettings,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => {
|
||||||
|
try {
|
||||||
|
await sendDocumentWithSubject(data);
|
||||||
|
|
||||||
|
if (data.meta.distributionMethod === DocumentDistributionMethod.EMAIL) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Document sent`),
|
title: _(msg`Document sent`),
|
||||||
description: _(msg`Your document has been sent successfully.`),
|
description: _(msg`Your document has been sent successfully.`),
|
||||||
@ -322,6 +379,21 @@ export const DocumentEditForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onAddSubjectFormAutoSave = async (data: TAddSubjectFormSchema) => {
|
||||||
|
try {
|
||||||
|
// Save form data without sending the document
|
||||||
|
await saveSubjectData(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: _(msg`Error`),
|
||||||
|
description: _(msg`An error occurred while auto-saving the subject form.`),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const currentDocumentFlow = documentFlow[step];
|
const currentDocumentFlow = documentFlow[step];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -367,25 +439,28 @@ export const DocumentEditForm = ({
|
|||||||
fields={fields}
|
fields={fields}
|
||||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
||||||
onSubmit={onAddSettingsFormSubmit}
|
onSubmit={onAddSettingsFormSubmit}
|
||||||
|
onAutoSave={onAddSettingsFormAutoSave}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AddSignersFormPartial
|
<AddSignersFormPartial
|
||||||
key={recipients.length}
|
key={document.id}
|
||||||
documentFlow={documentFlow.signers}
|
documentFlow={documentFlow.signers}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
signingOrder={document.documentMeta?.signingOrder}
|
signingOrder={document.documentMeta?.signingOrder}
|
||||||
allowDictateNextSigner={document.documentMeta?.allowDictateNextSigner}
|
allowDictateNextSigner={document.documentMeta?.allowDictateNextSigner}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
onSubmit={onAddSignersFormSubmit}
|
onSubmit={onAddSignersFormSubmit}
|
||||||
|
onAutoSave={onAddSignersFormAutoSave}
|
||||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AddFieldsFormPartial
|
<AddFieldsFormPartial
|
||||||
key={fields.length}
|
key={document.id}
|
||||||
documentFlow={documentFlow.fields}
|
documentFlow={documentFlow.fields}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
onSubmit={onAddFieldsFormSubmit}
|
onSubmit={onAddFieldsFormSubmit}
|
||||||
|
onAutoSave={onAddFieldsFormAutoSave}
|
||||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
||||||
teamId={team.id}
|
teamId={team.id}
|
||||||
/>
|
/>
|
||||||
@ -397,6 +472,7 @@ export const DocumentEditForm = ({
|
|||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
onSubmit={onAddSubjectFormSubmit}
|
onSubmit={onAddSubjectFormSubmit}
|
||||||
|
onAutoSave={onAddSubjectFormAutoSave}
|
||||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
||||||
/>
|
/>
|
||||||
</Stepper>
|
</Stepper>
|
||||||
|
|||||||
@ -42,7 +42,7 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
|
|||||||
|
|
||||||
const onDownloadClick = async () => {
|
const onDownloadClick = async () => {
|
||||||
try {
|
try {
|
||||||
const documentWithData = await trpcClient.document.getDocumentById.query(
|
const documentWithData = await trpcClient.document.get.query(
|
||||||
{
|
{
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -71,7 +71,7 @@ export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownP
|
|||||||
|
|
||||||
const onDownloadClick = async () => {
|
const onDownloadClick = async () => {
|
||||||
try {
|
try {
|
||||||
const documentWithData = await trpcClient.document.getDocumentById.query(
|
const documentWithData = await trpcClient.document.get.query(
|
||||||
{
|
{
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
},
|
},
|
||||||
@ -100,7 +100,7 @@ export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownP
|
|||||||
|
|
||||||
const onDownloadOriginalClick = async () => {
|
const onDownloadOriginalClick = async () => {
|
||||||
try {
|
try {
|
||||||
const documentWithData = await trpcClient.document.getDocumentById.query(
|
const documentWithData = await trpcClient.document.get.query(
|
||||||
{
|
{
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export const DocumentPageViewRecentActivity = ({
|
|||||||
hasNextPage,
|
hasNextPage,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
isFetchingNextPage,
|
isFetchingNextPage,
|
||||||
} = trpc.document.findDocumentAuditLogs.useInfiniteQuery(
|
} = trpc.document.auditLog.find.useInfiniteQuery(
|
||||||
{
|
{
|
||||||
documentId,
|
documentId,
|
||||||
filterForRecentActivity: true,
|
filterForRecentActivity: true,
|
||||||
|
|||||||
@ -52,7 +52,7 @@ export const DocumentUploadDropzone = ({ className }: DocumentUploadDropzoneProp
|
|||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const { mutateAsync: createDocument } = trpc.document.createDocument.useMutation();
|
const { mutateAsync: createDocument } = trpc.document.create.useMutation();
|
||||||
|
|
||||||
const disabledMessage = useMemo(() => {
|
const disabledMessage = useMemo(() => {
|
||||||
if (organisation.subscription && remaining.documents === 0) {
|
if (organisation.subscription && remaining.documents === 0) {
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export const LegacyFieldWarningPopover = ({
|
|||||||
const { mutateAsync: updateTemplate, isPending: isUpdatingTemplate } =
|
const { mutateAsync: updateTemplate, isPending: isUpdatingTemplate } =
|
||||||
trpc.template.updateTemplate.useMutation();
|
trpc.template.updateTemplate.useMutation();
|
||||||
const { mutateAsync: updateDocument, isPending: isUpdatingDocument } =
|
const { mutateAsync: updateDocument, isPending: isUpdatingDocument } =
|
||||||
trpc.document.updateDocument.useMutation();
|
trpc.document.update.useMutation();
|
||||||
|
|
||||||
const onUpdateFieldsClick = async () => {
|
const onUpdateFieldsClick = async () => {
|
||||||
if (type === 'document') {
|
if (type === 'document') {
|
||||||
|
|||||||
@ -124,32 +124,36 @@ export const TemplateEditForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onAddSettingsFormSubmit = async (data: TAddTemplateSettingsFormSchema) => {
|
const saveSettingsData = async (data: TAddTemplateSettingsFormSchema) => {
|
||||||
const { signatureTypes } = data.meta;
|
const { signatureTypes } = data.meta;
|
||||||
|
|
||||||
const parsedGlobalAccessAuth = z
|
const parsedGlobalAccessAuth = z
|
||||||
.array(ZDocumentAccessAuthTypesSchema)
|
.array(ZDocumentAccessAuthTypesSchema)
|
||||||
.safeParse(data.globalAccessAuth);
|
.safeParse(data.globalAccessAuth);
|
||||||
|
|
||||||
|
return updateTemplateSettings({
|
||||||
|
templateId: template.id,
|
||||||
|
data: {
|
||||||
|
title: data.title,
|
||||||
|
externalId: data.externalId || null,
|
||||||
|
visibility: data.visibility,
|
||||||
|
globalAccessAuth: parsedGlobalAccessAuth.success ? parsedGlobalAccessAuth.data : [],
|
||||||
|
globalActionAuth: data.globalActionAuth ?? [],
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
...data.meta,
|
||||||
|
emailReplyTo: data.meta.emailReplyTo || null,
|
||||||
|
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
|
||||||
|
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
||||||
|
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||||
|
language: isValidLanguageCode(data.meta.language) ? data.meta.language : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddSettingsFormSubmit = async (data: TAddTemplateSettingsFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await updateTemplateSettings({
|
await saveSettingsData(data);
|
||||||
templateId: template.id,
|
|
||||||
data: {
|
|
||||||
title: data.title,
|
|
||||||
externalId: data.externalId || null,
|
|
||||||
visibility: data.visibility,
|
|
||||||
globalAccessAuth: parsedGlobalAccessAuth.success ? parsedGlobalAccessAuth.data : [],
|
|
||||||
globalActionAuth: data.globalActionAuth ?? [],
|
|
||||||
},
|
|
||||||
meta: {
|
|
||||||
...data.meta,
|
|
||||||
emailReplyTo: data.meta.emailReplyTo || null,
|
|
||||||
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
|
|
||||||
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
|
||||||
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
|
||||||
language: isValidLanguageCode(data.meta.language) ? data.meta.language : undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setStep('signers');
|
setStep('signers');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -163,24 +167,42 @@ export const TemplateEditForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onAddSettingsFormAutoSave = async (data: TAddTemplateSettingsFormSchema) => {
|
||||||
|
try {
|
||||||
|
await saveSettingsData(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: _(msg`Error`),
|
||||||
|
description: _(msg`An error occurred while auto-saving the template settings.`),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveTemplatePlaceholderData = async (data: TAddTemplatePlacholderRecipientsFormSchema) => {
|
||||||
|
return Promise.all([
|
||||||
|
updateTemplateSettings({
|
||||||
|
templateId: template.id,
|
||||||
|
meta: {
|
||||||
|
signingOrder: data.signingOrder,
|
||||||
|
allowDictateNextSigner: data.allowDictateNextSigner,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
setRecipients({
|
||||||
|
templateId: template.id,
|
||||||
|
recipients: data.signers,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
const onAddTemplatePlaceholderFormSubmit = async (
|
const onAddTemplatePlaceholderFormSubmit = async (
|
||||||
data: TAddTemplatePlacholderRecipientsFormSchema,
|
data: TAddTemplatePlacholderRecipientsFormSchema,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await saveTemplatePlaceholderData(data);
|
||||||
updateTemplateSettings({
|
|
||||||
templateId: template.id,
|
|
||||||
meta: {
|
|
||||||
signingOrder: data.signingOrder,
|
|
||||||
allowDictateNextSigner: data.allowDictateNextSigner,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
setRecipients({
|
|
||||||
templateId: template.id,
|
|
||||||
recipients: data.signers,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
setStep('fields');
|
setStep('fields');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -192,12 +214,46 @@ export const TemplateEditForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onAddTemplatePlaceholderFormAutoSave = async (
|
||||||
|
data: TAddTemplatePlacholderRecipientsFormSchema,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await saveTemplatePlaceholderData(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: _(msg`Error`),
|
||||||
|
description: _(msg`An error occurred while auto-saving the template placeholders.`),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveFieldsData = async (data: TAddTemplateFieldsFormSchema) => {
|
||||||
|
return addTemplateFields({
|
||||||
|
templateId: template.id,
|
||||||
|
fields: data.fields,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddFieldsFormAutoSave = async (data: TAddTemplateFieldsFormSchema) => {
|
||||||
|
try {
|
||||||
|
await saveFieldsData(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: _(msg`Error`),
|
||||||
|
description: _(msg`An error occurred while auto-saving the template fields.`),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onAddFieldsFormSubmit = async (data: TAddTemplateFieldsFormSchema) => {
|
const onAddFieldsFormSubmit = async (data: TAddTemplateFieldsFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await addTemplateFields({
|
await saveFieldsData(data);
|
||||||
templateId: template.id,
|
|
||||||
fields: data.fields,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear all field data from localStorage
|
// Clear all field data from localStorage
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
@ -270,11 +326,12 @@ export const TemplateEditForm = ({
|
|||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
onSubmit={onAddSettingsFormSubmit}
|
onSubmit={onAddSettingsFormSubmit}
|
||||||
|
onAutoSave={onAddSettingsFormAutoSave}
|
||||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AddTemplatePlaceholderRecipientsFormPartial
|
<AddTemplatePlaceholderRecipientsFormPartial
|
||||||
key={recipients.length}
|
key={template.id}
|
||||||
documentFlow={documentFlow.signers}
|
documentFlow={documentFlow.signers}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
@ -282,15 +339,17 @@ export const TemplateEditForm = ({
|
|||||||
allowDictateNextSigner={template.templateMeta?.allowDictateNextSigner}
|
allowDictateNextSigner={template.templateMeta?.allowDictateNextSigner}
|
||||||
templateDirectLink={template.directLink}
|
templateDirectLink={template.directLink}
|
||||||
onSubmit={onAddTemplatePlaceholderFormSubmit}
|
onSubmit={onAddTemplatePlaceholderFormSubmit}
|
||||||
|
onAutoSave={onAddTemplatePlaceholderFormAutoSave}
|
||||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AddTemplateFieldsFormPartial
|
<AddTemplateFieldsFormPartial
|
||||||
key={fields.length}
|
key={template.id}
|
||||||
documentFlow={documentFlow.fields}
|
documentFlow={documentFlow.fields}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
onSubmit={onAddFieldsFormSubmit}
|
onSubmit={onAddFieldsFormSubmit}
|
||||||
|
onAutoSave={onAddFieldsFormAutoSave}
|
||||||
teamId={team?.id}
|
teamId={team?.id}
|
||||||
/>
|
/>
|
||||||
</Stepper>
|
</Stepper>
|
||||||
|
|||||||
@ -67,7 +67,7 @@ export const TemplatePageViewDocumentsTable = ({
|
|||||||
Object.fromEntries(searchParams ?? []),
|
Object.fromEntries(searchParams ?? []),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data, isLoading, isLoadingError } = trpc.document.findDocuments.useQuery(
|
const { data, isLoading, isLoadingError } = trpc.document.find.useQuery(
|
||||||
{
|
{
|
||||||
templateId,
|
templateId,
|
||||||
page: parsedSearchParams.page,
|
page: parsedSearchParams.page,
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export const TemplatePageViewRecentActivity = ({
|
|||||||
templateId,
|
templateId,
|
||||||
documentRootPath,
|
documentRootPath,
|
||||||
}: TemplatePageViewRecentActivityProps) => {
|
}: TemplatePageViewRecentActivityProps) => {
|
||||||
const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocuments.useQuery({
|
const { data, isLoading, isLoadingError, refetch } = trpc.document.find.useQuery({
|
||||||
templateId,
|
templateId,
|
||||||
orderByColumn: 'createdAt',
|
orderByColumn: 'createdAt',
|
||||||
orderByDirection: 'asc',
|
orderByDirection: 'asc',
|
||||||
|
|||||||
@ -52,7 +52,7 @@ export const AdminDocumentRecipientItemTable = ({ recipient }: RecipientItemProp
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: updateRecipient } = trpc.admin.updateRecipient.useMutation();
|
const { mutateAsync: updateRecipient } = trpc.admin.recipient.update.useMutation();
|
||||||
|
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export const DocumentLogsTable = ({ documentId }: DocumentLogsTableProps) => {
|
|||||||
|
|
||||||
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
|
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
|
||||||
|
|
||||||
const { data, isLoading, isLoadingError } = trpc.document.findDocumentAuditLogs.useQuery(
|
const { data, isLoading, isLoadingError } = trpc.document.auditLog.find.useQuery(
|
||||||
{
|
{
|
||||||
documentId,
|
documentId,
|
||||||
page: parsedSearchParams.page,
|
page: parsedSearchParams.page,
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonPr
|
|||||||
const onDownloadClick = async () => {
|
const onDownloadClick = async () => {
|
||||||
try {
|
try {
|
||||||
const document = !recipient
|
const document = !recipient
|
||||||
? await trpcClient.document.getDocumentById.query(
|
? await trpcClient.document.get.query(
|
||||||
{
|
{
|
||||||
documentId: row.id,
|
documentId: row.id,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -77,7 +77,7 @@ export const DocumentsTableActionDropdown = ({
|
|||||||
const onDownloadClick = async () => {
|
const onDownloadClick = async () => {
|
||||||
try {
|
try {
|
||||||
const document = !recipient
|
const document = !recipient
|
||||||
? await trpcClient.document.getDocumentById.query({
|
? await trpcClient.document.get.query({
|
||||||
documentId: row.id,
|
documentId: row.id,
|
||||||
})
|
})
|
||||||
: await trpcClient.document.getDocumentByToken.query({
|
: await trpcClient.document.getDocumentByToken.query({
|
||||||
@ -103,7 +103,7 @@ export const DocumentsTableActionDropdown = ({
|
|||||||
const onDownloadOriginalClick = async () => {
|
const onDownloadOriginalClick = async () => {
|
||||||
try {
|
try {
|
||||||
const document = !recipient
|
const document = !recipient
|
||||||
? await trpcClient.document.getDocumentById.query({
|
? await trpcClient.document.get.query({
|
||||||
documentId: row.id,
|
documentId: row.id,
|
||||||
})
|
})
|
||||||
: await trpcClient.document.getDocumentByToken.query({
|
: await trpcClient.document.getDocumentByToken.query({
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-upda
|
|||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import type { TFindDocumentsResponse } from '@documenso/trpc/server/document-router/schema';
|
import type { TFindDocumentsResponse } from '@documenso/trpc/server/document-router/find-documents.types';
|
||||||
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
||||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||||
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
|
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
|||||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type { TFindInboxResponse } from '@documenso/trpc/server/document-router/find-inbox.types';
|
import type { TFindInboxResponse } from '@documenso/trpc/server/document-router/find-inbox.types';
|
||||||
import type { TFindDocumentsResponse } from '@documenso/trpc/server/document-router/schema';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
||||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||||
@ -32,12 +31,12 @@ import { useOptionalCurrentTeam } from '~/providers/team';
|
|||||||
import { StackAvatarsWithTooltip } from '../general/stack-avatars-with-tooltip';
|
import { StackAvatarsWithTooltip } from '../general/stack-avatars-with-tooltip';
|
||||||
|
|
||||||
export type DocumentsTableProps = {
|
export type DocumentsTableProps = {
|
||||||
data?: TFindDocumentsResponse;
|
data?: TFindInboxResponse;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
isLoadingError?: boolean;
|
isLoadingError?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DocumentsTableRow = TFindDocumentsResponse['data'][number];
|
type DocumentsTableRow = TFindInboxResponse['data'][number];
|
||||||
|
|
||||||
export const InboxTable = () => {
|
export const InboxTable = () => {
|
||||||
const { _, i18n } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
|
|||||||
@ -62,7 +62,7 @@ export const SettingsSecurityPasskeyTableActions = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: updatePasskey, isPending: isUpdatingPasskey } =
|
const { mutateAsync: updatePasskey, isPending: isUpdatingPasskey } =
|
||||||
trpc.auth.updatePasskey.useMutation({
|
trpc.auth.passkey.update.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Success`),
|
title: _(msg`Success`),
|
||||||
@ -84,7 +84,7 @@ export const SettingsSecurityPasskeyTableActions = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: deletePasskey, isPending: isDeletingPasskey } =
|
const { mutateAsync: deletePasskey, isPending: isDeletingPasskey } =
|
||||||
trpc.auth.deletePasskey.useMutation({
|
trpc.auth.passkey.delete.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Success`),
|
title: _(msg`Success`),
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export const SettingsSecurityPasskeyTable = () => {
|
|||||||
|
|
||||||
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
|
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
|
||||||
|
|
||||||
const { data, isLoading, isLoadingError } = trpc.auth.findPasskeys.useQuery(
|
const { data, isLoading, isLoadingError } = trpc.auth.passkey.find.useQuery(
|
||||||
{
|
{
|
||||||
page: parsedSearchParams.page,
|
page: parsedSearchParams.page,
|
||||||
perPage: parsedSearchParams.perPage,
|
perPage: parsedSearchParams.perPage,
|
||||||
|
|||||||
@ -48,7 +48,7 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { mutate: resealDocument, isPending: isResealDocumentLoading } =
|
const { mutate: resealDocument, isPending: isResealDocumentLoading } =
|
||||||
trpc.admin.resealDocument.useMutation({
|
trpc.admin.document.reseal.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Success`),
|
title: _(msg`Success`),
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export default function AdminDocumentsPage() {
|
|||||||
const perPage = searchParams?.get?.('perPage') ? Number(searchParams.get('perPage')) : undefined;
|
const perPage = searchParams?.get?.('perPage') ? Number(searchParams.get('perPage')) : undefined;
|
||||||
|
|
||||||
const { data: findDocumentsData, isPending: isFindDocumentsLoading } =
|
const { data: findDocumentsData, isPending: isFindDocumentsLoading } =
|
||||||
trpc.admin.findDocuments.useQuery(
|
trpc.admin.document.find.useQuery(
|
||||||
{
|
{
|
||||||
query: debouncedTerm,
|
query: debouncedTerm,
|
||||||
page: page || 1,
|
page: page || 1,
|
||||||
|
|||||||
@ -2,14 +2,14 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { User } from '@prisma/client';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useRevalidator } from 'react-router';
|
import { useRevalidator } from 'react-router';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { ZAdminUpdateProfileMutationSchema } from '@documenso/trpc/server/admin-router/schema';
|
import type { TGetUserResponse } from '@documenso/trpc/server/admin-router/get-user.types';
|
||||||
|
import { ZUpdateUserRequestSchema } from '@documenso/trpc/server/admin-router/update-user.types';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@ -33,12 +33,12 @@ import { AdminOrganisationsTable } from '~/components/tables/admin-organisations
|
|||||||
|
|
||||||
import { MultiSelectRoleCombobox } from '../../../components/general/multiselect-role-combobox';
|
import { MultiSelectRoleCombobox } from '../../../components/general/multiselect-role-combobox';
|
||||||
|
|
||||||
const ZUserFormSchema = ZAdminUpdateProfileMutationSchema.omit({ id: true });
|
const ZUserFormSchema = ZUpdateUserRequestSchema.omit({ id: true });
|
||||||
|
|
||||||
type TUserFormSchema = z.infer<typeof ZUserFormSchema>;
|
type TUserFormSchema = z.infer<typeof ZUserFormSchema>;
|
||||||
|
|
||||||
export default function UserPage({ params }: { params: { id: number } }) {
|
export default function UserPage({ params }: { params: { id: number } }) {
|
||||||
const { data: user, isLoading: isLoadingUser } = trpc.profile.getUser.useQuery(
|
const { data: user, isLoading: isLoadingUser } = trpc.admin.user.get.useQuery(
|
||||||
{
|
{
|
||||||
id: Number(params.id),
|
id: Number(params.id),
|
||||||
},
|
},
|
||||||
@ -78,14 +78,14 @@ export default function UserPage({ params }: { params: { id: number } }) {
|
|||||||
return <AdminUserPage user={user} />;
|
return <AdminUserPage user={user} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AdminUserPage = ({ user }: { user: User }) => {
|
const AdminUserPage = ({ user }: { user: TGetUserResponse }) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { revalidate } = useRevalidator();
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const roles = user.roles ?? [];
|
const roles = user.roles ?? [];
|
||||||
|
|
||||||
const { mutateAsync: updateUserMutation } = trpc.admin.updateUser.useMutation();
|
const { mutateAsync: updateUserMutation } = trpc.admin.user.update.useMutation();
|
||||||
|
|
||||||
const form = useForm<TUserFormSchema>({
|
const form = useForm<TUserFormSchema>({
|
||||||
resolver: zodResolver(ZUserFormSchema),
|
resolver: zodResolver(ZUserFormSchema),
|
||||||
|
|||||||
@ -67,6 +67,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
|||||||
const documentVisibility = document?.visibility;
|
const documentVisibility = document?.visibility;
|
||||||
const currentTeamMemberRole = team.currentTeamRole;
|
const currentTeamMemberRole = team.currentTeamRole;
|
||||||
const isRecipient = document?.recipients.find((recipient) => recipient.email === user.email);
|
const isRecipient = document?.recipients.find((recipient) => recipient.email === user.email);
|
||||||
|
|
||||||
let canAccessDocument = true;
|
let canAccessDocument = true;
|
||||||
|
|
||||||
if (!isRecipient && document?.userId !== user.id) {
|
if (!isRecipient && document?.userId !== user.id) {
|
||||||
|
|||||||
@ -50,10 +50,6 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
|||||||
throw redirect(documentRootPath);
|
throw redirect(documentRootPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.folderId) {
|
|
||||||
throw redirect(documentRootPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipients = await getRecipientsForDocument({
|
const recipients = await getRecipientsForDocument({
|
||||||
documentId,
|
documentId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@ -68,13 +64,13 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
document,
|
document,
|
||||||
documentRootPath,
|
|
||||||
recipients,
|
recipients,
|
||||||
|
documentRootPath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps) {
|
export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps) {
|
||||||
const { document, documentRootPath, recipients } = loaderData;
|
const { document, recipients, documentRootPath } = loaderData;
|
||||||
|
|
||||||
const { _, i18n } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
|
|
||||||
|
|||||||
@ -12,10 +12,8 @@ import { parseToIntegerArray } from '@documenso/lib/utils/params';
|
|||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import {
|
import type { TFindDocumentsInternalResponse } from '@documenso/trpc/server/document-router/find-documents-internal.types';
|
||||||
type TFindDocumentsInternalResponse,
|
import { ZFindDocumentsInternalRequestSchema } from '@documenso/trpc/server/document-router/find-documents-internal.types';
|
||||||
ZFindDocumentsInternalRequestSchema,
|
|
||||||
} from '@documenso/trpc/server/document-router/schema';
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export function meta() {
|
|||||||
export default function ApiTokensPage() {
|
export default function ApiTokensPage() {
|
||||||
const { i18n } = useLingui();
|
const { i18n } = useLingui();
|
||||||
|
|
||||||
const { data: tokens } = trpc.apiToken.getTokens.useQuery();
|
const { data: tokens } = trpc.apiToken.getMany.useQuery();
|
||||||
|
|
||||||
const team = useOptionalCurrentTeam();
|
const team = useOptionalCurrentTeam();
|
||||||
|
|
||||||
|
|||||||
@ -9,10 +9,10 @@ import { trpc } from '@documenso/trpc/react';
|
|||||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||||
|
|
||||||
import { FolderGrid } from '~/components/general/folder/folder-grid';
|
import { FolderGrid } from '~/components/general/folder/folder-grid';
|
||||||
|
import { TemplateDropZoneWrapper } from '~/components/general/template/template-drop-zone-wrapper';
|
||||||
import { TemplatesTable } from '~/components/tables/templates-table';
|
import { TemplatesTable } from '~/components/tables/templates-table';
|
||||||
import { useCurrentTeam } from '~/providers/team';
|
import { useCurrentTeam } from '~/providers/team';
|
||||||
import { appMetaTags } from '~/utils/meta';
|
import { appMetaTags } from '~/utils/meta';
|
||||||
import { TemplateDropZoneWrapper } from '~/components/general/template/template-drop-zone-wrapper';
|
|
||||||
|
|
||||||
export function meta() {
|
export function meta() {
|
||||||
return appMetaTags('Templates');
|
return appMetaTags('Templates');
|
||||||
|
|||||||
@ -45,6 +45,9 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
|||||||
mode: 'insensitive',
|
mode: 'insensitive',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Directly convert the team member invite to a team member if they already have an account.
|
// Directly convert the team member invite to a team member if they already have an account.
|
||||||
|
|||||||
@ -101,5 +101,5 @@
|
|||||||
"vite-plugin-babel-macros": "^1.0.6",
|
"vite-plugin-babel-macros": "^1.0.6",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
},
|
},
|
||||||
"version": "1.12.2-rc.4"
|
"version": "1.12.2-rc.6"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,4 +51,4 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
volumes:
|
volumes:
|
||||||
- ../../apps/web/example/cert.p12:/opt/documenso/cert.p12
|
- ../../apps/remix/example/cert.p12:/opt/documenso/cert.p12
|
||||||
|
|||||||
6
package-lock.json
generated
6
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "1.12.2-rc.4",
|
"version": "1.12.2-rc.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "1.12.2-rc.4",
|
"version": "1.12.2-rc.6",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
@ -89,7 +89,7 @@
|
|||||||
},
|
},
|
||||||
"apps/remix": {
|
"apps/remix": {
|
||||||
"name": "@documenso/remix",
|
"name": "@documenso/remix",
|
||||||
"version": "1.12.2-rc.4",
|
"version": "1.12.2-rc.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/api": "*",
|
"@documenso/api": "*",
|
||||||
"@documenso/assets": "*",
|
"@documenso/assets": "*",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.12.2-rc.4",
|
"version": "1.12.2-rc.6",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"dev": "turbo run dev --filter=@documenso/remix",
|
"dev": "turbo run dev --filter=@documenso/remix",
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import { createTemplate } from '@documenso/lib/server-only/template/create-templ
|
|||||||
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
|
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
|
||||||
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
|
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
|
||||||
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||||
|
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
|
||||||
import { extractDerivedDocumentEmailSettings } from '@documenso/lib/types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '@documenso/lib/types/document-email';
|
||||||
import {
|
import {
|
||||||
ZCheckboxFieldMeta,
|
ZCheckboxFieldMeta,
|
||||||
@ -330,6 +331,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
formValues: body.formValues,
|
formValues: body.formValues,
|
||||||
|
folderId: body.folderId,
|
||||||
documentDataId: documentData.id,
|
documentDataId: documentData.id,
|
||||||
requestMetadata: metadata,
|
requestMetadata: metadata,
|
||||||
});
|
});
|
||||||
@ -736,6 +738,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
|||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
recipients: body.recipients,
|
recipients: body.recipients,
|
||||||
prefillFields: body.prefillFields,
|
prefillFields: body.prefillFields,
|
||||||
|
folderId: body.folderId,
|
||||||
override: {
|
override: {
|
||||||
title: body.title,
|
title: body.title,
|
||||||
...body.meta,
|
...body.meta,
|
||||||
@ -978,10 +981,12 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
recipients: [
|
recipients: [
|
||||||
...recipients.map(({ email, name }) => ({
|
...recipients.map((recipient) => ({
|
||||||
email,
|
email: recipient.email,
|
||||||
name,
|
name: recipient.name,
|
||||||
role,
|
role: recipient.role,
|
||||||
|
signingOrder: recipient.signingOrder,
|
||||||
|
actionAuth: ZRecipientAuthOptionsSchema.parse(recipient.authOptions)?.actionAuth ?? [],
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
email,
|
email,
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export const ZNoBodyMutationSchema = null;
|
|||||||
*/
|
*/
|
||||||
export const ZGetDocumentsQuerySchema = z.object({
|
export const ZGetDocumentsQuerySchema = z.object({
|
||||||
page: z.coerce.number().min(1).optional().default(1),
|
page: z.coerce.number().min(1).optional().default(1),
|
||||||
perPage: z.coerce.number().min(1).optional().default(1),
|
perPage: z.coerce.number().min(1).optional().default(10),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TGetDocumentsQuerySchema = z.infer<typeof ZGetDocumentsQuerySchema>;
|
export type TGetDocumentsQuerySchema = z.infer<typeof ZGetDocumentsQuerySchema>;
|
||||||
@ -136,6 +136,12 @@ export type TUploadDocumentSuccessfulSchema = z.infer<typeof ZUploadDocumentSucc
|
|||||||
export const ZCreateDocumentMutationSchema = z.object({
|
export const ZCreateDocumentMutationSchema = z.object({
|
||||||
title: z.string().min(1),
|
title: z.string().min(1),
|
||||||
externalId: z.string().nullish(),
|
externalId: z.string().nullish(),
|
||||||
|
folderId: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
'The ID of the folder to create the document in. If not provided, the document will be created in the root folder.',
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
recipients: z.array(
|
recipients: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
@ -287,6 +293,12 @@ export type TCreateDocumentFromTemplateMutationResponseSchema = z.infer<
|
|||||||
export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
|
export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
externalId: z.string().optional(),
|
externalId: z.string().optional(),
|
||||||
|
folderId: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
'The ID of the folder to create the document in. If not provided, the document will be created in the root folder.',
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
recipients: z
|
recipients: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
@ -625,5 +637,5 @@ export const ZSuccessfulGetTemplatesResponseSchema = z.object({
|
|||||||
|
|
||||||
export const ZGetTemplatesQuerySchema = z.object({
|
export const ZGetTemplatesQuerySchema = z.object({
|
||||||
page: z.coerce.number().min(1).optional().default(1),
|
page: z.coerce.number().min(1).optional().default(1),
|
||||||
perPage: z.coerce.number().min(1).optional().default(1),
|
perPage: z.coerce.number().min(1).optional().default(10),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,293 @@
|
|||||||
|
import type { Page } from '@playwright/test';
|
||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
|
||||||
|
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
|
||||||
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
|
||||||
|
const setupDocumentAndNavigateToFieldsStep = async (page: Page) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
const document = await seedBlankDocument(user, team.id);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/documents/${document.id}/edit`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||||
|
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Add signer' }).click();
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Email').nth(1).fill('recipient2@documenso.com');
|
||||||
|
await page.getByPlaceholder('Name').nth(1).fill('Recipient 2');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
return { user, team, document };
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerAutosave = async (page: Page) => {
|
||||||
|
await page.locator('#document-flow-form-container').click();
|
||||||
|
await page.locator('#document-flow-form-container').blur();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
test.describe('AutoSave Fields Step', () => {
|
||||||
|
test('should autosave the fields without advanced settings', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocumentAndNavigateToFieldsStep(page);
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Signature' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Text' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId('field-advanced-settings-footer')
|
||||||
|
.getByRole('button', { name: 'Cancel' })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByRole('combobox').click();
|
||||||
|
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Signature' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 500,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedFields = await getFieldsForDocument({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedFields.length).toBe(3);
|
||||||
|
expect(retrievedFields[0].type).toBe('SIGNATURE');
|
||||||
|
expect(retrievedFields[1].type).toBe('TEXT');
|
||||||
|
expect(retrievedFields[2].type).toBe('SIGNATURE');
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the field deletion', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocumentAndNavigateToFieldsStep(page);
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Signature' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Text' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId('field-advanced-settings-footer')
|
||||||
|
.getByRole('button', { name: 'Cancel' })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByRole('combobox').click();
|
||||||
|
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Signature' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 500,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByRole('combobox').click();
|
||||||
|
await page.getByRole('option', { name: 'Recipient 1 (recipient1@documenso.com)' }).click();
|
||||||
|
|
||||||
|
await page.getByText('Text').nth(1).click();
|
||||||
|
await page.getByRole('button', { name: 'Remove' }).click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedFields = await getFieldsForDocument({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedFields.length).toBe(2);
|
||||||
|
expect(retrievedFields[0].type).toBe('SIGNATURE');
|
||||||
|
expect(retrievedFields[1].type).toBe('SIGNATURE');
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the field duplication', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocumentAndNavigateToFieldsStep(page);
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Signature' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Text' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId('field-advanced-settings-footer')
|
||||||
|
.getByRole('button', { name: 'Cancel' })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByRole('combobox').click();
|
||||||
|
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Signature' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 500,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByRole('combobox').click();
|
||||||
|
await page.getByRole('option', { name: 'Recipient 1 (recipient1@documenso.com)' }).click();
|
||||||
|
|
||||||
|
await page.getByText('Signature').nth(1).click();
|
||||||
|
await page.getByRole('button', { name: 'Duplicate', exact: true }).click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedFields = await getFieldsForDocument({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedFields.length).toBe(4);
|
||||||
|
expect(retrievedFields[0].type).toBe('SIGNATURE');
|
||||||
|
expect(retrievedFields[1].type).toBe('TEXT');
|
||||||
|
expect(retrievedFields[2].type).toBe('SIGNATURE');
|
||||||
|
expect(retrievedFields[3].type).toBe('SIGNATURE');
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the fields with advanced settings', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocumentAndNavigateToFieldsStep(page);
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Signature' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Text' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('textbox', { name: 'Field label' }).fill('Test Field');
|
||||||
|
await page.getByRole('textbox', { name: 'Field placeholder' }).fill('Test Placeholder');
|
||||||
|
await page.getByRole('textbox', { name: 'Add text to the field' }).fill('Test Text');
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId('field-advanced-settings-footer')
|
||||||
|
.getByRole('button', { name: 'Save' })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedFields = await getFieldsForDocument({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedFields.length).toBe(2);
|
||||||
|
expect(retrievedFields[0].type).toBe('SIGNATURE');
|
||||||
|
expect(retrievedFields[1].type).toBe('TEXT');
|
||||||
|
|
||||||
|
const textField = retrievedFields[1];
|
||||||
|
expect(textField.fieldMeta).toBeDefined();
|
||||||
|
|
||||||
|
if (
|
||||||
|
textField.fieldMeta &&
|
||||||
|
typeof textField.fieldMeta === 'object' &&
|
||||||
|
'type' in textField.fieldMeta
|
||||||
|
) {
|
||||||
|
expect(textField.fieldMeta.type).toBe('text');
|
||||||
|
expect(textField.fieldMeta.label).toBe('Test Field');
|
||||||
|
expect(textField.fieldMeta.placeholder).toBe('Test Placeholder');
|
||||||
|
|
||||||
|
if (textField.fieldMeta.type === 'text') {
|
||||||
|
expect(textField.fieldMeta.text).toBe('Test Text');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('fieldMeta should be defined and contain advanced settings');
|
||||||
|
}
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,243 @@
|
|||||||
|
import type { Page } from '@playwright/test';
|
||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||||
|
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
|
||||||
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
|
||||||
|
test.describe.configure({ mode: 'parallel', timeout: 60000 });
|
||||||
|
|
||||||
|
const setupDocument = async (page: Page) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
|
||||||
|
const document = await seedBlankDocument(user, team.id);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/documents/${document.id}/edit`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { user, team, document };
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerAutosave = async (page: Page) => {
|
||||||
|
await page.locator('#document-flow-form-container').click();
|
||||||
|
await page.locator('#document-flow-form-container').blur();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
test.describe('AutoSave Settings Step', () => {
|
||||||
|
test('should autosave the title change', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocument(page);
|
||||||
|
|
||||||
|
const newDocumentTitle = 'New Document Title';
|
||||||
|
|
||||||
|
await page.getByRole('textbox', { name: 'Title *' }).fill(newDocumentTitle);
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrieved = await getDocumentById({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.getByRole('textbox', { name: 'Title *' })).toHaveValue(retrieved.title);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the language change', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocument(page);
|
||||||
|
|
||||||
|
const newDocumentLanguage = 'French';
|
||||||
|
const expectedLanguageCode = 'fr';
|
||||||
|
|
||||||
|
await page.getByRole('combobox').first().click();
|
||||||
|
await page.getByRole('option', { name: newDocumentLanguage }).click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrieved = await getDocumentById({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrieved.documentMeta?.language).toBe(expectedLanguageCode);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the document access change', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocument(page);
|
||||||
|
|
||||||
|
const access = 'Require account';
|
||||||
|
const accessValue = 'ACCOUNT';
|
||||||
|
|
||||||
|
await page.getByRole('combobox').nth(1).click();
|
||||||
|
await page.getByRole('option', { name: access }).click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrieved = await getDocumentById({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrieved.authOptions?.globalAccessAuth).toContain(accessValue);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the external ID change', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocument(page);
|
||||||
|
|
||||||
|
const newExternalId = '1234567890';
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('textbox', { name: 'External ID' }).fill(newExternalId);
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrieved = await getDocumentById({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrieved.externalId).toBe(newExternalId);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the allowed signature types change', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocument(page);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('combobox').nth(3).click();
|
||||||
|
await page.getByRole('option', { name: 'Draw' }).click();
|
||||||
|
await page.getByRole('option', { name: 'Type' }).click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrieved = await getDocumentById({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrieved.documentMeta?.drawSignatureEnabled).toBe(false);
|
||||||
|
expect(retrieved.documentMeta?.typedSignatureEnabled).toBe(false);
|
||||||
|
expect(retrieved.documentMeta?.uploadSignatureEnabled).toBe(true);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the date format change', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocument(page);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('combobox').nth(4).click();
|
||||||
|
await page.getByRole('option', { name: 'ISO 8601', exact: true }).click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrieved = await getDocumentById({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrieved.documentMeta?.dateFormat).toBe("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the timezone change', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocument(page);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('combobox').nth(5).click();
|
||||||
|
await page.getByRole('option', { name: 'Europe/London' }).click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrieved = await getDocumentById({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrieved.documentMeta?.timezone).toBe('Europe/London');
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the redirect URL change', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocument(page);
|
||||||
|
|
||||||
|
const newRedirectUrl = 'https://documenso.com/test/';
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('textbox', { name: 'Redirect URL' }).fill(newRedirectUrl);
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrieved = await getDocumentById({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrieved.documentMeta?.redirectUrl).toBe(newRedirectUrl);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave multiple field changes together', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocument(page);
|
||||||
|
|
||||||
|
const newTitle = 'Updated Document Title';
|
||||||
|
await page.getByRole('textbox', { name: 'Title *' }).fill(newTitle);
|
||||||
|
|
||||||
|
await page.getByRole('combobox').first().click();
|
||||||
|
await page.getByRole('option', { name: 'German' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('combobox').nth(1).click();
|
||||||
|
await page.getByRole('option', { name: 'Require account' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||||
|
const newExternalId = 'MULTI-TEST-123';
|
||||||
|
await page.getByRole('textbox', { name: 'External ID' }).fill(newExternalId);
|
||||||
|
|
||||||
|
await page.getByRole('combobox').nth(5).click();
|
||||||
|
await page.getByRole('option', { name: 'Europe/Berlin' }).click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrieved = await getDocumentById({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrieved.title).toBe(newTitle);
|
||||||
|
expect(retrieved.documentMeta?.language).toBe('de');
|
||||||
|
expect(retrieved.authOptions?.globalAccessAuth).toContain('ACCOUNT');
|
||||||
|
expect(retrieved.externalId).toBe(newExternalId);
|
||||||
|
expect(retrieved.documentMeta?.timezone).toBe('Europe/Berlin');
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,168 @@
|
|||||||
|
import type { Page } from '@playwright/test';
|
||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||||
|
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||||
|
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
|
||||||
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
|
||||||
|
test.describe.configure({ mode: 'parallel', timeout: 60000 });
|
||||||
|
|
||||||
|
const setupDocumentAndNavigateToSignersStep = async (page: Page) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
const document = await seedBlankDocument(user, team.id);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/documents/${document.id}/edit`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
return { user, team, document };
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerAutosave = async (page: Page) => {
|
||||||
|
await page.locator('#document-flow-form-container').click();
|
||||||
|
await page.locator('#document-flow-form-container').blur();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addSignerAndSave = async (page: Page) => {
|
||||||
|
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||||
|
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
};
|
||||||
|
|
||||||
|
test.describe('AutoSave Signers Step', () => {
|
||||||
|
test('should autosave the signers addition', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocumentAndNavigateToSignersStep(page);
|
||||||
|
|
||||||
|
await addSignerAndSave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedRecipients = await getRecipientsForDocument({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedRecipients.length).toBe(1);
|
||||||
|
expect(retrievedRecipients[0].email).toBe('recipient1@documenso.com');
|
||||||
|
expect(retrievedRecipients[0].name).toBe('Recipient 1');
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the signer deletion', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocumentAndNavigateToSignersStep(page);
|
||||||
|
|
||||||
|
await addSignerAndSave(page);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Add myself' }).click();
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByTestId('remove-signer-button').first().click();
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedRecipients = await getRecipientsForDocument({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedRecipients.length).toBe(1);
|
||||||
|
expect(retrievedRecipients[0].email).toBe(user.email);
|
||||||
|
expect(retrievedRecipients[0].name).toBe(user.name);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the signer update', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocumentAndNavigateToSignersStep(page);
|
||||||
|
|
||||||
|
await addSignerAndSave(page);
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Name').fill('Documenso Manager');
|
||||||
|
await page.getByPlaceholder('Email').fill('manager@documenso.com');
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByRole('combobox').click();
|
||||||
|
await page.getByRole('option', { name: 'Receives copy' }).click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedRecipients = await getRecipientsForDocument({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedRecipients.length).toBe(1);
|
||||||
|
expect(retrievedRecipients[0].email).toBe('manager@documenso.com');
|
||||||
|
expect(retrievedRecipients[0].name).toBe('Documenso Manager');
|
||||||
|
expect(retrievedRecipients[0].role).toBe('CC');
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the signing order change', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocumentAndNavigateToSignersStep(page);
|
||||||
|
|
||||||
|
await addSignerAndSave(page);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Add signer' }).click();
|
||||||
|
|
||||||
|
await page.getByTestId('signer-email-input').nth(1).fill('recipient2@documenso.com');
|
||||||
|
await page.getByLabel('Name').nth(1).fill('Recipient 2');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||||
|
|
||||||
|
await page.getByTestId('signer-email-input').nth(2).fill('recipient3@documenso.com');
|
||||||
|
await page.getByLabel('Name').nth(2).fill('Recipient 3');
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByLabel('Enable signing order').check();
|
||||||
|
await page.getByLabel('Allow signers to dictate next signer').check();
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByTestId('signing-order-input').nth(0).fill('3');
|
||||||
|
await page.getByTestId('signing-order-input').nth(0).blur();
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByTestId('signing-order-input').nth(1).fill('1');
|
||||||
|
await page.getByTestId('signing-order-input').nth(1).blur();
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByTestId('signing-order-input').nth(2).fill('2');
|
||||||
|
await page.getByTestId('signing-order-input').nth(2).blur();
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedDocumentData = await getDocumentById({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const retrievedRecipients = await getRecipientsForDocument({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedDocumentData.documentMeta?.signingOrder).toBe('SEQUENTIAL');
|
||||||
|
expect(retrievedDocumentData.documentMeta?.allowDictateNextSigner).toBe(true);
|
||||||
|
expect(retrievedRecipients.length).toBe(3);
|
||||||
|
expect(retrievedRecipients[0].signingOrder).toBe(2);
|
||||||
|
expect(retrievedRecipients[1].signingOrder).toBe(3);
|
||||||
|
expect(retrievedRecipients[2].signingOrder).toBe(1);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,200 @@
|
|||||||
|
import type { Page } from '@playwright/test';
|
||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||||
|
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
|
||||||
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
|
||||||
|
test.describe.configure({ mode: 'parallel', timeout: 60000 });
|
||||||
|
|
||||||
|
export const setupDocumentAndNavigateToSubjectStep = async (page: Page) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
const document = await seedBlankDocument(user, team.id);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/documents/${document.id}/edit`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||||
|
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Signature' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Distribute Document' })).toBeVisible();
|
||||||
|
|
||||||
|
return { user, team, document };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const triggerAutosave = async (page: Page) => {
|
||||||
|
await page.locator('#document-flow-form-container').click();
|
||||||
|
await page.locator('#document-flow-form-container').blur();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
test.describe('AutoSave Subject Step', () => {
|
||||||
|
test('should autosave the subject field', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocumentAndNavigateToSubjectStep(page);
|
||||||
|
|
||||||
|
const subject = 'Hello world!';
|
||||||
|
|
||||||
|
await page.getByRole('textbox', { name: 'Subject (Optional)' }).fill(subject);
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedDocumentData = await getDocumentById({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.getByRole('textbox', { name: 'Subject (Optional)' })).toHaveValue(
|
||||||
|
retrievedDocumentData.documentMeta?.subject ?? '',
|
||||||
|
);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the message field', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocumentAndNavigateToSubjectStep(page);
|
||||||
|
|
||||||
|
const message = 'Please review and sign this important document. Thank you!';
|
||||||
|
|
||||||
|
await page.getByRole('textbox', { name: 'Message (Optional)' }).fill(message);
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedDocumentData = await getDocumentById({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.getByRole('textbox', { name: 'Message (Optional)' })).toHaveValue(
|
||||||
|
retrievedDocumentData.documentMeta?.message ?? '',
|
||||||
|
);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the email settings checkboxes', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocumentAndNavigateToSubjectStep(page);
|
||||||
|
|
||||||
|
// Toggle some email settings checkboxes (randomly - some checked, some unchecked)
|
||||||
|
await page.getByText('Send recipient signed email').click();
|
||||||
|
await page.getByText('Send recipient removed email').click();
|
||||||
|
await page.getByText('Send document completed email', { exact: true }).click();
|
||||||
|
await page.getByText('Send document deleted email').click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedDocumentData = await getDocumentById({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emailSettings = retrievedDocumentData.documentMeta?.emailSettings;
|
||||||
|
|
||||||
|
await expect(page.getByText('Send recipient signed email')).toBeChecked({
|
||||||
|
checked: emailSettings?.recipientSigned,
|
||||||
|
});
|
||||||
|
await expect(page.getByText('Send recipient removed email')).toBeChecked({
|
||||||
|
checked: emailSettings?.recipientRemoved,
|
||||||
|
});
|
||||||
|
await expect(page.getByText('Send document completed email', { exact: true })).toBeChecked({
|
||||||
|
checked: emailSettings?.documentCompleted,
|
||||||
|
});
|
||||||
|
await expect(page.getByText('Send document deleted email')).toBeChecked({
|
||||||
|
checked: emailSettings?.documentDeleted,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.getByText('Send recipient signing request email')).toBeChecked({
|
||||||
|
checked: emailSettings?.recipientSigningRequest,
|
||||||
|
});
|
||||||
|
await expect(page.getByText('Send document pending email')).toBeChecked({
|
||||||
|
checked: emailSettings?.documentPending,
|
||||||
|
});
|
||||||
|
await expect(page.getByText('Send document completed email to the owner')).toBeChecked({
|
||||||
|
checked: emailSettings?.ownerDocumentCompleted,
|
||||||
|
});
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave all fields and settings together', async ({ page }) => {
|
||||||
|
const { user, document, team } = await setupDocumentAndNavigateToSubjectStep(page);
|
||||||
|
|
||||||
|
const subject = 'Combined Test Subject - Please Sign';
|
||||||
|
const message =
|
||||||
|
'This is a comprehensive test message for autosave functionality. Please review and sign at your earliest convenience.';
|
||||||
|
|
||||||
|
await page.getByRole('textbox', { name: 'Subject (Optional)' }).fill(subject);
|
||||||
|
await page.getByRole('textbox', { name: 'Message (Optional)' }).fill(message);
|
||||||
|
|
||||||
|
await page.getByText('Send recipient signed email').click();
|
||||||
|
await page.getByText('Send recipient removed email').click();
|
||||||
|
await page.getByText('Send document completed email', { exact: true }).click();
|
||||||
|
await page.getByText('Send document deleted email').click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedDocumentData = await getDocumentById({
|
||||||
|
documentId: document.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedDocumentData.documentMeta?.subject).toBe(subject);
|
||||||
|
expect(retrievedDocumentData.documentMeta?.message).toBe(message);
|
||||||
|
expect(retrievedDocumentData.documentMeta?.emailSettings).toBeDefined();
|
||||||
|
|
||||||
|
await expect(page.getByRole('textbox', { name: 'Subject (Optional)' })).toHaveValue(
|
||||||
|
retrievedDocumentData.documentMeta?.subject ?? '',
|
||||||
|
);
|
||||||
|
await expect(page.getByRole('textbox', { name: 'Message (Optional)' })).toHaveValue(
|
||||||
|
retrievedDocumentData.documentMeta?.message ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(page.getByText('Send recipient signed email')).toBeChecked({
|
||||||
|
checked: retrievedDocumentData.documentMeta?.emailSettings?.recipientSigned,
|
||||||
|
});
|
||||||
|
await expect(page.getByText('Send recipient removed email')).toBeChecked({
|
||||||
|
checked: retrievedDocumentData.documentMeta?.emailSettings?.recipientRemoved,
|
||||||
|
});
|
||||||
|
await expect(page.getByText('Send document completed email', { exact: true })).toBeChecked({
|
||||||
|
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentCompleted,
|
||||||
|
});
|
||||||
|
await expect(page.getByText('Send document deleted email')).toBeChecked({
|
||||||
|
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentDeleted,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.getByText('Send recipient signing request email')).toBeChecked({
|
||||||
|
checked: retrievedDocumentData.documentMeta?.emailSettings?.recipientSigningRequest,
|
||||||
|
});
|
||||||
|
await expect(page.getByText('Send document pending email')).toBeChecked({
|
||||||
|
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentPending,
|
||||||
|
});
|
||||||
|
await expect(page.getByText('Send document completed email to the owner')).toBeChecked({
|
||||||
|
checked: retrievedDocumentData.documentMeta?.emailSettings?.ownerDocumentCompleted,
|
||||||
|
});
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -534,9 +534,6 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip
|
|||||||
await page.getByLabel('Title').fill(documentTitle);
|
await page.getByLabel('Title').fill(documentTitle);
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
|
||||||
await page.getByLabel('Enable signing order').check();
|
|
||||||
|
|
||||||
for (let i = 1; i <= 3; i++) {
|
for (let i = 1; i <= 3; i++) {
|
||||||
if (i > 1) {
|
if (i > 1) {
|
||||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||||
@ -558,6 +555,9 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip
|
|||||||
.fill(`User ${i}`);
|
.fill(`User ${i}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||||
|
await page.getByLabel('Enable signing order').check();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||||
|
|||||||
@ -0,0 +1,304 @@
|
|||||||
|
import type { Page } from '@playwright/test';
|
||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||||
|
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
||||||
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
|
||||||
|
const setupTemplateAndNavigateToFieldsStep = async (page: Page) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
const template = await seedBlankTemplate(user, team.id);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/templates/${template.id}/edit`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||||
|
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Email').nth(1).fill('recipient2@documenso.com');
|
||||||
|
await page.getByPlaceholder('Name').nth(1).fill('Recipient 2');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
return { user, team, template };
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerAutosave = async (page: Page) => {
|
||||||
|
await page.locator('#document-flow-form-container').click();
|
||||||
|
await page.locator('#document-flow-form-container').blur();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
test.describe('AutoSave Fields Step', () => {
|
||||||
|
test('should autosave the fields without advanced settings', async ({ page }) => {
|
||||||
|
const { user, template, team } = await setupTemplateAndNavigateToFieldsStep(page);
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Signature' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Text' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId('field-advanced-settings-footer')
|
||||||
|
.getByRole('button', { name: 'Cancel' })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByRole('combobox').click();
|
||||||
|
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Signature' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 500,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedFields = await getTemplateById({
|
||||||
|
id: template.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fields = retrievedFields.fields;
|
||||||
|
|
||||||
|
expect(fields.length).toBe(3);
|
||||||
|
expect(fields[0].type).toBe('SIGNATURE');
|
||||||
|
expect(fields[1].type).toBe('TEXT');
|
||||||
|
expect(fields[2].type).toBe('SIGNATURE');
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the field deletion', async ({ page }) => {
|
||||||
|
const { user, template, team } = await setupTemplateAndNavigateToFieldsStep(page);
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Signature' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Text' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId('field-advanced-settings-footer')
|
||||||
|
.getByRole('button', { name: 'Cancel' })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByRole('combobox').click();
|
||||||
|
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Signature' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 500,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByRole('combobox').click();
|
||||||
|
await page.getByRole('option', { name: 'Recipient 1 (recipient1@documenso.com)' }).click();
|
||||||
|
|
||||||
|
await page.getByText('Text').nth(1).click();
|
||||||
|
await page.getByRole('button', { name: 'Remove' }).click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedFields = await getTemplateById({
|
||||||
|
id: template.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fields = retrievedFields.fields;
|
||||||
|
|
||||||
|
expect(fields.length).toBe(2);
|
||||||
|
expect(fields[0].type).toBe('SIGNATURE');
|
||||||
|
expect(fields[1].type).toBe('SIGNATURE');
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the field duplication', async ({ page }) => {
|
||||||
|
const { user, template, team } = await setupTemplateAndNavigateToFieldsStep(page);
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Signature' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Text' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId('field-advanced-settings-footer')
|
||||||
|
.getByRole('button', { name: 'Cancel' })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByRole('combobox').click();
|
||||||
|
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Signature' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 500,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByRole('combobox').click();
|
||||||
|
await page.getByRole('option', { name: 'Recipient 1 (recipient1@documenso.com)' }).click();
|
||||||
|
|
||||||
|
await page.getByText('Signature').nth(1).click();
|
||||||
|
await page.getByRole('button', { name: 'Duplicate', exact: true }).click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedFields = await getTemplateById({
|
||||||
|
id: template.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fields = retrievedFields.fields;
|
||||||
|
|
||||||
|
expect(fields.length).toBe(4);
|
||||||
|
expect(fields[0].type).toBe('SIGNATURE');
|
||||||
|
expect(fields[1].type).toBe('TEXT');
|
||||||
|
expect(fields[2].type).toBe('SIGNATURE');
|
||||||
|
expect(fields[3].type).toBe('SIGNATURE');
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the fields with advanced settings', async ({ page }) => {
|
||||||
|
const { user, template, team } = await setupTemplateAndNavigateToFieldsStep(page);
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Signature' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Text' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('textbox', { name: 'Field label' }).fill('Test Field');
|
||||||
|
await page.getByRole('textbox', { name: 'Field placeholder' }).fill('Test Placeholder');
|
||||||
|
await page.getByRole('textbox', { name: 'Add text to the field' }).fill('Test Text');
|
||||||
|
|
||||||
|
await page.getByTestId('field-advanced-settings-footer').waitFor({ state: 'visible' });
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId('field-advanced-settings-footer')
|
||||||
|
.getByRole('button', { name: 'Save' })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(2500);
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedTemplate = await getTemplateById({
|
||||||
|
id: template.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fields = retrievedTemplate.fields;
|
||||||
|
|
||||||
|
expect(fields.length).toBe(2);
|
||||||
|
expect(fields[0].type).toBe('SIGNATURE');
|
||||||
|
expect(fields[1].type).toBe('TEXT');
|
||||||
|
|
||||||
|
const textField = fields[1];
|
||||||
|
expect(textField.fieldMeta).toBeDefined();
|
||||||
|
|
||||||
|
if (
|
||||||
|
textField.fieldMeta &&
|
||||||
|
typeof textField.fieldMeta === 'object' &&
|
||||||
|
'type' in textField.fieldMeta
|
||||||
|
) {
|
||||||
|
expect(textField.fieldMeta.type).toBe('text');
|
||||||
|
expect(textField.fieldMeta.label).toBe('Test Field');
|
||||||
|
expect(textField.fieldMeta.placeholder).toBe('Test Placeholder');
|
||||||
|
|
||||||
|
if (textField.fieldMeta.type === 'text') {
|
||||||
|
expect(textField.fieldMeta.text).toBe('Test Text');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('fieldMeta should be defined and contain advanced settings');
|
||||||
|
}
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,244 @@
|
|||||||
|
import type { Page } from '@playwright/test';
|
||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||||
|
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
||||||
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
|
||||||
|
test.describe.configure({ mode: 'parallel', timeout: 60000 });
|
||||||
|
|
||||||
|
const setupTemplate = async (page: Page) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
const template = await seedBlankTemplate(user, team.id);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/templates/${template.id}/edit`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { user, team, template };
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerAutosave = async (page: Page) => {
|
||||||
|
await page.locator('#document-flow-form-container').click();
|
||||||
|
await page.locator('#document-flow-form-container').blur();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
test.describe('AutoSave Settings Step - Templates', () => {
|
||||||
|
test('should autosave the title change', async ({ page }) => {
|
||||||
|
const { user, template, team } = await setupTemplate(page);
|
||||||
|
|
||||||
|
const newTemplateTitle = 'New Template Title';
|
||||||
|
|
||||||
|
await page.getByRole('textbox', { name: 'Title *' }).fill(newTemplateTitle);
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedTemplate = await getTemplateById({
|
||||||
|
id: template.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.getByRole('textbox', { name: 'Title *' })).toHaveValue(
|
||||||
|
retrievedTemplate.title,
|
||||||
|
);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the language change', async ({ page }) => {
|
||||||
|
const { user, template, team } = await setupTemplate(page);
|
||||||
|
|
||||||
|
const newTemplateLanguage = 'French';
|
||||||
|
const expectedLanguageCode = 'fr';
|
||||||
|
|
||||||
|
await page.getByRole('combobox').first().click();
|
||||||
|
await page.getByRole('option', { name: newTemplateLanguage }).click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedTemplate = await getTemplateById({
|
||||||
|
id: template.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedTemplate.templateMeta?.language).toBe(expectedLanguageCode);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the template access change', async ({ page }) => {
|
||||||
|
const { user, template, team } = await setupTemplate(page);
|
||||||
|
|
||||||
|
const access = 'Require account';
|
||||||
|
const accessValue = 'ACCOUNT';
|
||||||
|
|
||||||
|
await page.getByRole('combobox').nth(1).click();
|
||||||
|
await page.getByRole('option', { name: access }).click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedTemplate = await getTemplateById({
|
||||||
|
id: template.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedTemplate.authOptions?.globalAccessAuth).toContain(accessValue);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the external ID change', async ({ page }) => {
|
||||||
|
const { user, template, team } = await setupTemplate(page);
|
||||||
|
|
||||||
|
const newExternalId = '1234567890';
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('textbox', { name: 'External ID' }).fill(newExternalId);
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedTemplate = await getTemplateById({
|
||||||
|
id: template.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedTemplate.externalId).toBe(newExternalId);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the allowed signature types change', async ({ page }) => {
|
||||||
|
const { user, template, team } = await setupTemplate(page);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('combobox').nth(4).click();
|
||||||
|
await page.getByRole('option', { name: 'Draw' }).click();
|
||||||
|
await page.getByRole('option', { name: 'Type' }).click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedTemplate = await getTemplateById({
|
||||||
|
id: template.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedTemplate.templateMeta?.drawSignatureEnabled).toBe(false);
|
||||||
|
expect(retrievedTemplate.templateMeta?.typedSignatureEnabled).toBe(false);
|
||||||
|
expect(retrievedTemplate.templateMeta?.uploadSignatureEnabled).toBe(true);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the date format change', async ({ page }) => {
|
||||||
|
const { user, template, team } = await setupTemplate(page);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('combobox').nth(5).click();
|
||||||
|
await page.getByRole('option', { name: 'ISO 8601', exact: true }).click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedTemplate = await getTemplateById({
|
||||||
|
id: template.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedTemplate.templateMeta?.dateFormat).toBe("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the timezone change', async ({ page }) => {
|
||||||
|
const { user, template, team } = await setupTemplate(page);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('combobox').nth(6).click();
|
||||||
|
await page.getByRole('option', { name: 'Europe/London' }).click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedTemplate = await getTemplateById({
|
||||||
|
id: template.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedTemplate.templateMeta?.timezone).toBe('Europe/London');
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the redirect URL change', async ({ page }) => {
|
||||||
|
const { user, template, team } = await setupTemplate(page);
|
||||||
|
|
||||||
|
const newRedirectUrl = 'https://documenso.com/test/';
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('textbox', { name: 'Redirect URL' }).fill(newRedirectUrl);
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedTemplate = await getTemplateById({
|
||||||
|
id: template.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedTemplate.templateMeta?.redirectUrl).toBe(newRedirectUrl);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave multiple field changes together', async ({ page }) => {
|
||||||
|
const { user, template, team } = await setupTemplate(page);
|
||||||
|
|
||||||
|
const newTitle = 'Updated Template Title';
|
||||||
|
await page.getByRole('textbox', { name: 'Title *' }).fill(newTitle);
|
||||||
|
|
||||||
|
await page.getByRole('combobox').first().click();
|
||||||
|
await page.getByRole('option', { name: 'German' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('combobox').nth(1).click();
|
||||||
|
await page.getByRole('option', { name: 'Require account' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||||
|
const newExternalId = 'MULTI-TEST-123';
|
||||||
|
await page.getByRole('textbox', { name: 'External ID' }).fill(newExternalId);
|
||||||
|
|
||||||
|
await page.getByRole('combobox').nth(6).click();
|
||||||
|
await page.getByRole('option', { name: 'Europe/Berlin' }).click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedTemplate = await getTemplateById({
|
||||||
|
id: template.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedTemplate.title).toBe(newTitle);
|
||||||
|
expect(retrievedTemplate.templateMeta?.language).toBe('de');
|
||||||
|
expect(retrievedTemplate.authOptions?.globalAccessAuth).toContain('ACCOUNT');
|
||||||
|
expect(retrievedTemplate.externalId).toBe(newExternalId);
|
||||||
|
expect(retrievedTemplate.templateMeta?.timezone).toBe('Europe/Berlin');
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,174 @@
|
|||||||
|
import type { Page } from '@playwright/test';
|
||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
import { getRecipientsForTemplate } from '@documenso/lib/server-only/recipient/get-recipients-for-template';
|
||||||
|
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||||
|
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
||||||
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
|
||||||
|
test.describe.configure({ mode: 'parallel', timeout: 60000 });
|
||||||
|
|
||||||
|
const setupTemplateAndNavigateToSignersStep = async (page: Page) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
const template = await seedBlankTemplate(user, team.id);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/templates/${template.id}/edit`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
return { user, team, template };
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerAutosave = async (page: Page) => {
|
||||||
|
await page.locator('#document-flow-form-container').click();
|
||||||
|
await page.locator('#document-flow-form-container').blur();
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addSignerAndSave = async (page: Page) => {
|
||||||
|
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||||
|
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
};
|
||||||
|
|
||||||
|
test.describe('AutoSave Signers Step - Templates', () => {
|
||||||
|
test('should autosave the signers addition', async ({ page }) => {
|
||||||
|
const { user, template, team } = await setupTemplateAndNavigateToSignersStep(page);
|
||||||
|
|
||||||
|
await addSignerAndSave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedRecipients = await getRecipientsForTemplate({
|
||||||
|
templateId: template.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedRecipients.length).toBe(1);
|
||||||
|
expect(retrievedRecipients[0].email).toBe('recipient1@documenso.com');
|
||||||
|
expect(retrievedRecipients[0].name).toBe('Recipient 1');
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the signer deletion', async ({ page }) => {
|
||||||
|
const { user, template, team } = await setupTemplateAndNavigateToSignersStep(page);
|
||||||
|
|
||||||
|
await addSignerAndSave(page);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Add myself' }).click();
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByTestId('remove-placeholder-recipient-button').first().click();
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedRecipients = await getRecipientsForTemplate({
|
||||||
|
templateId: template.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedRecipients.length).toBe(1);
|
||||||
|
expect(retrievedRecipients[0].email).toBe(user.email);
|
||||||
|
expect(retrievedRecipients[0].name).toBe(user.name);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the signer update', async ({ page }) => {
|
||||||
|
const { user, template, team } = await setupTemplateAndNavigateToSignersStep(page);
|
||||||
|
|
||||||
|
await addSignerAndSave(page);
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Name').fill('Documenso Manager');
|
||||||
|
await page.getByPlaceholder('Email').fill('manager@documenso.com');
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByRole('combobox').click();
|
||||||
|
await page.getByRole('option', { name: 'Receives copy' }).click();
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedRecipients = await getRecipientsForTemplate({
|
||||||
|
templateId: template.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedRecipients.length).toBe(1);
|
||||||
|
expect(retrievedRecipients[0].email).toBe('manager@documenso.com');
|
||||||
|
expect(retrievedRecipients[0].name).toBe('Documenso Manager');
|
||||||
|
expect(retrievedRecipients[0].role).toBe('CC');
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should autosave the signing order change', async ({ page }) => {
|
||||||
|
const { user, template, team } = await setupTemplateAndNavigateToSignersStep(page);
|
||||||
|
|
||||||
|
await addSignerAndSave(page);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Add placeholder recipient' }).click();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId('placeholder-recipient-email-input')
|
||||||
|
.nth(1)
|
||||||
|
.fill('recipient2@documenso.com');
|
||||||
|
await page.getByTestId('placeholder-recipient-name-input').nth(1).fill('Recipient 2');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Add placeholder recipient' }).click();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId('placeholder-recipient-email-input')
|
||||||
|
.nth(2)
|
||||||
|
.fill('recipient3@documenso.com');
|
||||||
|
await page.getByTestId('placeholder-recipient-name-input').nth(2).fill('Recipient 3');
|
||||||
|
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByLabel('Enable signing order').check();
|
||||||
|
await page.getByLabel('Allow signers to dictate next signer').check();
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByTestId('placeholder-recipient-signing-order-input').nth(0).fill('3');
|
||||||
|
await page.getByTestId('placeholder-recipient-signing-order-input').nth(0).blur();
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByTestId('placeholder-recipient-signing-order-input').nth(1).fill('1');
|
||||||
|
await page.getByTestId('placeholder-recipient-signing-order-input').nth(1).blur();
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await page.getByTestId('placeholder-recipient-signing-order-input').nth(2).fill('2');
|
||||||
|
await page.getByTestId('placeholder-recipient-signing-order-input').nth(2).blur();
|
||||||
|
await triggerAutosave(page);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const retrievedTemplate = await getTemplateById({
|
||||||
|
id: template.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const retrievedRecipients = await getRecipientsForTemplate({
|
||||||
|
templateId: template.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retrievedTemplate.templateMeta?.signingOrder).toBe('SEQUENTIAL');
|
||||||
|
expect(retrievedTemplate.templateMeta?.allowDictateNextSigner).toBe(true);
|
||||||
|
expect(retrievedRecipients.length).toBe(3);
|
||||||
|
expect(retrievedRecipients[0].signingOrder).toBe(2);
|
||||||
|
expect(retrievedRecipients[1].signingOrder).toBe(3);
|
||||||
|
expect(retrievedRecipients[2].signingOrder).toBe(1);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -17,7 +17,7 @@ export default defineConfig({
|
|||||||
testDir: './e2e',
|
testDir: './e2e',
|
||||||
/* Run tests in files in parallel */
|
/* Run tests in files in parallel */
|
||||||
fullyParallel: false,
|
fullyParallel: false,
|
||||||
workers: 1,
|
workers: 4,
|
||||||
maxFailures: process.env.CI ? 1 : undefined,
|
maxFailures: process.env.CI ? 1 : undefined,
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
|
|||||||
@ -92,7 +92,11 @@ export const handleOAuthCallbackUrl = async (options: HandleOAuthCallbackUrlOpti
|
|||||||
providerAccountId: sub,
|
providerAccountId: sub,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
user: true,
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -107,6 +111,10 @@ export const handleOAuthCallbackUrl = async (options: HandleOAuthCallbackUrlOpti
|
|||||||
where: {
|
where: {
|
||||||
email: email,
|
email: email,
|
||||||
},
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
emailVerified: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle existing user but no account.
|
// Handle existing user but no account.
|
||||||
|
|||||||
31
packages/lib/client-only/hooks/use-autosave.ts
Normal file
31
packages/lib/client-only/hooks/use-autosave.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export const useAutoSave = <T>(onSave: (data: T) => Promise<void>) => {
|
||||||
|
const saveTimeoutRef = useRef<NodeJS.Timeout>();
|
||||||
|
|
||||||
|
const saveFormData = async (data: T) => {
|
||||||
|
try {
|
||||||
|
await onSave(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Auto-save failed:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleSave = useCallback((data: T) => {
|
||||||
|
if (saveTimeoutRef.current) {
|
||||||
|
clearTimeout(saveTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveTimeoutRef.current = setTimeout(() => void saveFormData(data), 2000);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (saveTimeoutRef.current) {
|
||||||
|
clearTimeout(saveTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { scheduleSave };
|
||||||
|
};
|
||||||
@ -29,7 +29,13 @@ export const run = async ({
|
|||||||
id: documentId,
|
id: documentId,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
user: true,
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
documentMeta: true,
|
documentMeta: true,
|
||||||
recipients: true,
|
recipients: true,
|
||||||
team: {
|
team: {
|
||||||
|
|||||||
@ -39,7 +39,13 @@ export const run = async ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
user: true,
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -51,7 +57,13 @@ export const run = async ({
|
|||||||
organisationId: payload.organisationId,
|
organisationId: payload.organisationId,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
user: true,
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -39,7 +39,13 @@ export const run = async ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
user: true,
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -49,6 +55,11 @@ export const run = async ({
|
|||||||
where: {
|
where: {
|
||||||
id: payload.memberUserId,
|
id: payload.memberUserId,
|
||||||
},
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { branding, emailLanguage, senderEmail } = await getEmailContext({
|
const { branding, emailLanguage, senderEmail } = await getEmailContext({
|
||||||
|
|||||||
@ -38,7 +38,13 @@ export const run = async ({
|
|||||||
id: recipientId,
|
id: recipientId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
user: true,
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
documentMeta: true,
|
documentMeta: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -33,7 +33,13 @@ export const run = async ({
|
|||||||
id: documentId,
|
id: documentId,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
user: true,
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
documentMeta: true,
|
documentMeta: true,
|
||||||
team: {
|
team: {
|
||||||
select: {
|
select: {
|
||||||
|
|||||||
@ -42,6 +42,11 @@ export const run = async ({
|
|||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
},
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
prisma.document.findFirstOrThrow({
|
prisma.document.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@ -10,13 +10,7 @@ export type UpdateUserOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const updateUser = async ({ id, name, email, roles }: UpdateUserOptions) => {
|
export const updateUser = async ({ id, name, email, roles }: UpdateUserOptions) => {
|
||||||
await prisma.user.findFirstOrThrow({
|
await prisma.user.update({
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return await prisma.user.update({
|
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { DocumentVisibility, TemplateMeta } from '@prisma/client';
|
import type { DocumentVisibility, TemplateMeta } from '@prisma/client';
|
||||||
import {
|
import {
|
||||||
DocumentSource,
|
DocumentSource,
|
||||||
|
FolderType,
|
||||||
RecipientRole,
|
RecipientRole,
|
||||||
SendStatus,
|
SendStatus,
|
||||||
SigningStatus,
|
SigningStatus,
|
||||||
@ -14,7 +15,7 @@ import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-reques
|
|||||||
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import type { TCreateDocumentV2Request } from '@documenso/trpc/server/document-router/schema';
|
import type { TCreateDocumentTemporaryRequest } from '@documenso/trpc/server/document-router/create-document-temporary.types';
|
||||||
|
|
||||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||||
import type { TDocumentFormValues } from '../../types/document-form-values';
|
import type { TDocumentFormValues } from '../../types/document-form-values';
|
||||||
@ -44,7 +45,8 @@ export type CreateDocumentOptions = {
|
|||||||
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||||
globalActionAuth?: TDocumentActionAuthTypes[];
|
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||||
formValues?: TDocumentFormValues;
|
formValues?: TDocumentFormValues;
|
||||||
recipients: TCreateDocumentV2Request['recipients'];
|
recipients: TCreateDocumentTemporaryRequest['recipients'];
|
||||||
|
folderId?: string;
|
||||||
};
|
};
|
||||||
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>;
|
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>;
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata: ApiRequestMetadata;
|
||||||
@ -59,7 +61,7 @@ export const createDocumentV2 = async ({
|
|||||||
meta,
|
meta,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
}: CreateDocumentOptions) => {
|
}: CreateDocumentOptions) => {
|
||||||
const { title, formValues } = data;
|
const { title, formValues, folderId } = data;
|
||||||
|
|
||||||
const team = await prisma.team.findFirst({
|
const team = await prisma.team.findFirst({
|
||||||
where: buildTeamWhereQuery({ teamId, userId }),
|
where: buildTeamWhereQuery({ teamId, userId }),
|
||||||
@ -78,6 +80,22 @@ export const createDocumentV2 = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (folderId) {
|
||||||
|
const folder = await prisma.folder.findUnique({
|
||||||
|
where: {
|
||||||
|
id: folderId,
|
||||||
|
type: FolderType.DOCUMENT,
|
||||||
|
team: buildTeamWhereQuery({ teamId, userId }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!folder) {
|
||||||
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||||
|
message: 'Folder not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const settings = await getTeamSettings({
|
const settings = await getTeamSettings({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
@ -164,6 +182,7 @@ export const createDocumentV2 = async ({
|
|||||||
teamId,
|
teamId,
|
||||||
authOptions,
|
authOptions,
|
||||||
visibility,
|
visibility,
|
||||||
|
folderId,
|
||||||
formValues,
|
formValues,
|
||||||
source: DocumentSource.DOCUMENT,
|
source: DocumentSource.DOCUMENT,
|
||||||
documentMeta: {
|
documentMeta: {
|
||||||
|
|||||||
@ -49,6 +49,11 @@ export const findDocuments = async ({
|
|||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
},
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let team = null;
|
let team = null;
|
||||||
@ -267,7 +272,7 @@ export const findDocuments = async ({
|
|||||||
|
|
||||||
const findDocumentsFilter = (
|
const findDocumentsFilter = (
|
||||||
status: ExtendedDocumentStatus,
|
status: ExtendedDocumentStatus,
|
||||||
user: User,
|
user: Pick<User, 'id' | 'email' | 'name'>,
|
||||||
folderId?: string | null,
|
folderId?: string | null,
|
||||||
) => {
|
) => {
|
||||||
return match<ExtendedDocumentStatus, Prisma.DocumentWhereInput>(status)
|
return match<ExtendedDocumentStatus, Prisma.DocumentWhereInput>(status)
|
||||||
|
|||||||
@ -73,7 +73,13 @@ export const getDocumentAndSenderByToken = async ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
user: true,
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
documentData: true,
|
documentData: true,
|
||||||
documentMeta: true,
|
documentMeta: true,
|
||||||
recipients: {
|
recipients: {
|
||||||
@ -90,9 +96,6 @@ export const getDocumentAndSenderByToken = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
|
||||||
const { password: _password, ...user } = result.user;
|
|
||||||
|
|
||||||
const recipient = result.recipients[0];
|
const recipient = result.recipients[0];
|
||||||
|
|
||||||
// Sanity check, should not be possible.
|
// Sanity check, should not be possible.
|
||||||
@ -120,7 +123,11 @@ export const getDocumentAndSenderByToken = async ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
user,
|
user: {
|
||||||
|
id: result.user.id,
|
||||||
|
email: result.user.email,
|
||||||
|
name: result.user.name,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -7,14 +7,12 @@ export type GetDocumentWithDetailsByIdOptions = {
|
|||||||
documentId: number;
|
documentId: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId: number;
|
teamId: number;
|
||||||
folderId?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDocumentWithDetailsById = async ({
|
export const getDocumentWithDetailsById = async ({
|
||||||
documentId,
|
documentId,
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
folderId,
|
|
||||||
}: GetDocumentWithDetailsByIdOptions) => {
|
}: GetDocumentWithDetailsByIdOptions) => {
|
||||||
const { documentWhereInput } = await getDocumentWhereInput({
|
const { documentWhereInput } = await getDocumentWhereInput({
|
||||||
documentId,
|
documentId,
|
||||||
@ -25,7 +23,6 @@ export const getDocumentWithDetailsById = async ({
|
|||||||
const document = await prisma.document.findFirst({
|
const document = await prisma.document.findFirst({
|
||||||
where: {
|
where: {
|
||||||
...documentWhereInput,
|
...documentWhereInput,
|
||||||
folderId,
|
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
documentData: true,
|
documentData: true,
|
||||||
|
|||||||
@ -28,13 +28,7 @@ export async function rejectDocumentWithToken({
|
|||||||
documentId,
|
documentId,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
document: {
|
document: true,
|
||||||
include: {
|
|
||||||
user: true,
|
|
||||||
recipients: true,
|
|
||||||
documentMeta: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -33,7 +33,13 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
|||||||
documentData: true,
|
documentData: true,
|
||||||
documentMeta: true,
|
documentMeta: true,
|
||||||
recipients: true,
|
recipients: true,
|
||||||
user: true,
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
team: {
|
team: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
|||||||
@ -24,7 +24,13 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
|||||||
id: documentId,
|
id: documentId,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
user: true,
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
documentMeta: true,
|
documentMeta: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -148,33 +148,6 @@ export const sendDocument = async ({
|
|||||||
// throw new Error('Some signers have not been assigned a signature field.');
|
// throw new Error('Some signers have not been assigned a signature field.');
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
|
|
||||||
document.documentMeta,
|
|
||||||
).recipientSigningRequest;
|
|
||||||
|
|
||||||
// Only send email if one of the following is true:
|
|
||||||
// - It is explicitly set
|
|
||||||
// - The email is enabled for signing requests AND sendEmail is undefined
|
|
||||||
if (sendEmail || (isRecipientSigningRequestEmailEnabled && sendEmail === undefined)) {
|
|
||||||
await Promise.all(
|
|
||||||
recipientsToNotify.map(async (recipient) => {
|
|
||||||
if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await jobs.triggerJob({
|
|
||||||
name: 'send.signing.requested.email',
|
|
||||||
payload: {
|
|
||||||
userId,
|
|
||||||
documentId,
|
|
||||||
recipientId: recipient.id,
|
|
||||||
requestMetadata: requestMetadata?.requestMetadata,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const allRecipientsHaveNoActionToTake = document.recipients.every(
|
const allRecipientsHaveNoActionToTake = document.recipients.every(
|
||||||
(recipient) =>
|
(recipient) =>
|
||||||
recipient.role === RecipientRole.CC || recipient.signingStatus === SigningStatus.SIGNED,
|
recipient.role === RecipientRole.CC || recipient.signingStatus === SigningStatus.SIGNED,
|
||||||
@ -227,6 +200,33 @@ export const sendDocument = async ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||||
|
document.documentMeta,
|
||||||
|
).recipientSigningRequest;
|
||||||
|
|
||||||
|
// Only send email if one of the following is true:
|
||||||
|
// - It is explicitly set
|
||||||
|
// - The email is enabled for signing requests AND sendEmail is undefined
|
||||||
|
if (sendEmail || (isRecipientSigningRequestEmailEnabled && sendEmail === undefined)) {
|
||||||
|
await Promise.all(
|
||||||
|
recipientsToNotify.map(async (recipient) => {
|
||||||
|
if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await jobs.triggerJob({
|
||||||
|
name: 'send.signing.requested.email',
|
||||||
|
payload: {
|
||||||
|
userId,
|
||||||
|
documentId,
|
||||||
|
recipientId: recipient.id,
|
||||||
|
requestMetadata: requestMetadata?.requestMetadata,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await triggerWebhook({
|
await triggerWebhook({
|
||||||
event: WebhookTriggerEvents.DOCUMENT_SENT,
|
event: WebhookTriggerEvents.DOCUMENT_SENT,
|
||||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
||||||
|
|||||||
@ -30,7 +30,13 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
|||||||
include: {
|
include: {
|
||||||
recipients: true,
|
recipients: true,
|
||||||
documentMeta: true,
|
documentMeta: true,
|
||||||
user: true,
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { P, match } from 'ts-pattern';
|
||||||
|
|
||||||
import type { BrandingSettings } from '@documenso/email/providers/branding';
|
import type { BrandingSettings } from '@documenso/email/providers/branding';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import type {
|
import type {
|
||||||
@ -104,7 +106,12 @@ export const getEmailContext = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const replyToEmail = meta?.emailReplyTo || emailContext.settings.emailReplyTo || undefined;
|
const replyToEmail = meta?.emailReplyTo || emailContext.settings.emailReplyTo || undefined;
|
||||||
const senderEmailId = meta?.emailId === null ? null : emailContext.settings.emailId;
|
|
||||||
|
const senderEmailId = match(meta?.emailId)
|
||||||
|
.with(P.string, (emailId) => emailId) // Explicit string means to use the provided email ID.
|
||||||
|
.with(undefined, () => emailContext.settings.emailId) // Undefined means to use the inherited email ID.
|
||||||
|
.with(null, () => null) // Explicit null means to use the Documenso email.
|
||||||
|
.exhaustive();
|
||||||
|
|
||||||
const foundSenderEmail = emailContext.allowedEmails.find((email) => email.id === senderEmailId);
|
const foundSenderEmail = emailContext.allowedEmails.find((email) => email.id === senderEmailId);
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type { PDFDocument } from 'pdf-lib';
|
|||||||
import { TextAlignment, rgb, setFontAndSize } from 'pdf-lib';
|
import { TextAlignment, rgb, setFontAndSize } from 'pdf-lib';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
|
import { getPageSize } from './get-page-size';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a rejection stamp to each page of a PDF document.
|
* Adds a rejection stamp to each page of a PDF document.
|
||||||
@ -27,7 +28,7 @@ export async function addRejectionStampToPdf(
|
|||||||
|
|
||||||
for (let i = 0; i < pages.length; i++) {
|
for (let i = 0; i < pages.length; i++) {
|
||||||
const page = pages[i];
|
const page = pages[i];
|
||||||
const { width, height } = page.getSize();
|
const { width, height } = getPageSize(page);
|
||||||
|
|
||||||
// Draw the "REJECTED" text
|
// Draw the "REJECTED" text
|
||||||
const rejectedTitleText = 'DOCUMENT REJECTED';
|
const rejectedTitleText = 'DOCUMENT REJECTED';
|
||||||
|
|||||||
18
packages/lib/server-only/pdf/get-page-size.ts
Normal file
18
packages/lib/server-only/pdf/get-page-size.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { PDFPage } from 'pdf-lib';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the effective page size for PDF operations.
|
||||||
|
*
|
||||||
|
* Uses CropBox by default to handle rare cases where MediaBox is larger than CropBox.
|
||||||
|
* Falls back to MediaBox when it's smaller than CropBox, following typical PDF reader behavior.
|
||||||
|
*/
|
||||||
|
export const getPageSize = (page: PDFPage) => {
|
||||||
|
const cropBox = page.getCropBox();
|
||||||
|
const mediaBox = page.getMediaBox();
|
||||||
|
|
||||||
|
if (mediaBox.width < cropBox.width || mediaBox.height < cropBox.height) {
|
||||||
|
return mediaBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cropBox;
|
||||||
|
};
|
||||||
@ -33,6 +33,7 @@ import {
|
|||||||
ZRadioFieldMeta,
|
ZRadioFieldMeta,
|
||||||
ZTextFieldMeta,
|
ZTextFieldMeta,
|
||||||
} from '../../types/field-meta';
|
} from '../../types/field-meta';
|
||||||
|
import { getPageSize } from './get-page-size';
|
||||||
|
|
||||||
export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
||||||
const [fontCaveat, fontNoto] = await Promise.all([
|
const [fontCaveat, fontNoto] = await Promise.all([
|
||||||
@ -77,7 +78,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
|||||||
|
|
||||||
const isPageRotatedToLandscape = pageRotationInDegrees === 90 || pageRotationInDegrees === 270;
|
const isPageRotatedToLandscape = pageRotationInDegrees === 90 || pageRotationInDegrees === 270;
|
||||||
|
|
||||||
let { width: pageWidth, height: pageHeight } = page.getSize();
|
let { width: pageWidth, height: pageHeight } = getPageSize(page);
|
||||||
|
|
||||||
// PDFs can have pages that are rotated, which are correctly rendered in the frontend.
|
// PDFs can have pages that are rotated, which are correctly rendered in the frontend.
|
||||||
// However when we load the PDF in the backend, the rotation is applied.
|
// However when we load the PDF in the backend, the rotation is applied.
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import {
|
|||||||
ZRadioFieldMeta,
|
ZRadioFieldMeta,
|
||||||
ZTextFieldMeta,
|
ZTextFieldMeta,
|
||||||
} from '../../types/field-meta';
|
} from '../../types/field-meta';
|
||||||
|
import { getPageSize } from './get-page-size';
|
||||||
|
|
||||||
export const legacy_insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
export const legacy_insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
||||||
const [fontCaveat, fontNoto] = await Promise.all([
|
const [fontCaveat, fontNoto] = await Promise.all([
|
||||||
@ -63,7 +64,7 @@ export const legacy_insertFieldInPDF = async (pdf: PDFDocument, field: FieldWith
|
|||||||
|
|
||||||
const isPageRotatedToLandscape = pageRotationInDegrees === 90 || pageRotationInDegrees === 270;
|
const isPageRotatedToLandscape = pageRotationInDegrees === 90 || pageRotationInDegrees === 270;
|
||||||
|
|
||||||
let { width: pageWidth, height: pageHeight } = page.getSize();
|
let { width: pageWidth, height: pageHeight } = getPageSize(page);
|
||||||
|
|
||||||
// PDFs can have pages that are rotated, which are correctly rendered in the frontend.
|
// PDFs can have pages that are rotated, which are correctly rendered in the frontend.
|
||||||
// However when we load the PDF in the backend, the rotation is applied.
|
// However when we load the PDF in the backend, the rotation is applied.
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export const deleteTokenById = async ({ id, userId, teamId }: DeleteTokenByIdOpt
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return await prisma.apiToken.delete({
|
await prisma.apiToken.delete({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
teamId,
|
teamId,
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||||
|
|
||||||
export interface GetRecipientsForTemplateOptions {
|
export interface GetRecipientsForTemplateOptions {
|
||||||
templateId: number;
|
templateId: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
@ -14,21 +16,12 @@ export const getRecipientsForTemplate = async ({
|
|||||||
const recipients = await prisma.recipient.findMany({
|
const recipients = await prisma.recipient.findMany({
|
||||||
where: {
|
where: {
|
||||||
templateId,
|
templateId,
|
||||||
template: teamId
|
template: {
|
||||||
? {
|
team: buildTeamWhereQuery({
|
||||||
team: {
|
teamId,
|
||||||
id: teamId,
|
userId,
|
||||||
members: {
|
}),
|
||||||
some: {
|
},
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
userId,
|
|
||||||
teamId: null,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
id: 'asc',
|
id: 'asc',
|
||||||
|
|||||||
@ -105,7 +105,13 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
directLink: true,
|
directLink: true,
|
||||||
templateDocumentData: true,
|
templateDocumentData: true,
|
||||||
templateMeta: true,
|
templateMeta: true,
|
||||||
user: true,
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/c
|
|||||||
import {
|
import {
|
||||||
DocumentSource,
|
DocumentSource,
|
||||||
type Field,
|
type Field,
|
||||||
|
FolderType,
|
||||||
type Recipient,
|
type Recipient,
|
||||||
RecipientRole,
|
RecipientRole,
|
||||||
SendStatus,
|
SendStatus,
|
||||||
@ -69,6 +70,7 @@ export type CreateDocumentFromTemplateOptions = {
|
|||||||
email: string;
|
email: string;
|
||||||
signingOrder?: number | null;
|
signingOrder?: number | null;
|
||||||
}[];
|
}[];
|
||||||
|
folderId?: string;
|
||||||
prefillFields?: TFieldMetaPrefillFieldsSchema[];
|
prefillFields?: TFieldMetaPrefillFieldsSchema[];
|
||||||
customDocumentDataId?: string;
|
customDocumentDataId?: string;
|
||||||
|
|
||||||
@ -274,6 +276,7 @@ export const createDocumentFromTemplate = async ({
|
|||||||
customDocumentDataId,
|
customDocumentDataId,
|
||||||
override,
|
override,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
|
folderId,
|
||||||
prefillFields,
|
prefillFields,
|
||||||
}: CreateDocumentFromTemplateOptions) => {
|
}: CreateDocumentFromTemplateOptions) => {
|
||||||
const template = await prisma.template.findUnique({
|
const template = await prisma.template.findUnique({
|
||||||
@ -298,6 +301,22 @@ export const createDocumentFromTemplate = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (folderId) {
|
||||||
|
const folder = await prisma.folder.findUnique({
|
||||||
|
where: {
|
||||||
|
id: folderId,
|
||||||
|
type: FolderType.DOCUMENT,
|
||||||
|
team: buildTeamWhereQuery({ teamId, userId }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!folder) {
|
||||||
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||||
|
message: 'Folder not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const settings = await getTeamSettings({
|
const settings = await getTeamSettings({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
@ -368,6 +387,7 @@ export const createDocumentFromTemplate = async ({
|
|||||||
externalId: externalId || template.externalId,
|
externalId: externalId || template.externalId,
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
userId,
|
userId,
|
||||||
|
folderId,
|
||||||
teamId: template.teamId,
|
teamId: template.teamId,
|
||||||
title: override?.title || template.title,
|
title: override?.title || template.title,
|
||||||
documentDataId: documentData.id,
|
documentDataId: documentData.id,
|
||||||
|
|||||||
@ -1,13 +1,31 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
|
|
||||||
export interface GetUserByIdOptions {
|
export interface GetUserByIdOptions {
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getUserById = async ({ id }: GetUserByIdOptions) => {
|
export const getUserById = async ({ id }: GetUserByIdOptions) => {
|
||||||
return await prisma.user.findFirstOrThrow({
|
const user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
emailVerified: true,
|
||||||
|
roles: true,
|
||||||
|
disabled: true,
|
||||||
|
twoFactorEnabled: true,
|
||||||
|
signature: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user