fix: merge conflicts

This commit is contained in:
Ephraim Atta-Duncan
2025-08-27 12:05:21 +00:00
185 changed files with 3075 additions and 1662 deletions

View File

@ -136,3 +136,5 @@ E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_Password123"
# OPTIONAL: The file to save the logger output to. Will disable stdout if provided. # OPTIONAL: The file to save the logger output to. Will disable stdout if provided.
NEXT_PRIVATE_LOGGER_FILE_PATH= NEXT_PRIVATE_LOGGER_FILE_PATH=
# [[PLAIN SUPPORT]]
NEXT_PRIVATE_PLAIN_API_KEY=

57
AGENTS.md Normal file
View 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

View File

@ -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

View File

@ -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

View File

@ -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>"
``` ```

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -0,0 +1,159 @@
import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useRevalidator } from 'react-router';
import { match } from 'ts-pattern';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
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 { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type AdminUserResetTwoFactorDialogProps = {
className?: string;
user: TGetUserResponse;
};
export const AdminUserResetTwoFactorDialog = ({
className,
user,
}: AdminUserResetTwoFactorDialogProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const [email, setEmail] = useState('');
const [open, setOpen] = useState(false);
const { mutateAsync: resetTwoFactor, isPending: isResettingTwoFactor } =
trpc.admin.user.resetTwoFactor.useMutation();
const onResetTwoFactor = async () => {
try {
await resetTwoFactor({
userId: user.id,
});
toast({
title: _(msg`2FA Reset`),
description: _(msg`The user's two factor authentication has been reset successfully.`),
duration: 5000,
});
await revalidate();
setOpen(false);
} catch (err) {
const error = AppError.parseError(err);
const errorMessage = match(error.code)
.with(AppErrorCode.NOT_FOUND, () => msg`User not found.`)
.with(
AppErrorCode.UNAUTHORIZED,
() => msg`You are not authorized to reset two factor authentcation for this user.`,
)
.otherwise(
() => msg`An error occurred while resetting two factor authentication for the user.`,
);
toast({
title: _(msg`Error`),
description: _(errorMessage),
variant: 'destructive',
duration: 7500,
});
}
};
const handleOpenChange = (newOpen: boolean) => {
setOpen(newOpen);
if (!newOpen) {
setEmail('');
}
};
return (
<div className={className}>
<Alert
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<div>
<AlertTitle>Reset Two Factor Authentication</AlertTitle>
<AlertDescription className="mr-2">
<Trans>
Reset the users two factor authentication. This action is irreversible and will
disable two factor authentication for the user.
</Trans>
</AlertDescription>
</div>
<div className="flex-shrink-0">
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button variant="destructive">
<Trans>Reset 2FA</Trans>
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader className="space-y-4">
<DialogTitle>
<Trans>Reset Two Factor Authentication</Trans>
</DialogTitle>
</DialogHeader>
<Alert variant="destructive">
<AlertDescription className="selection:bg-red-100">
<Trans>
This action is irreversible. Please ensure you have informed the user before
proceeding.
</Trans>
</AlertDescription>
</Alert>
<div>
<DialogDescription>
<Trans>
To confirm, please enter the accounts email address <br />({user.email}).
</Trans>
</DialogDescription>
<Input
className="mt-2"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<DialogFooter>
<Button
variant="destructive"
disabled={email !== user.email}
onClick={onResetTwoFactor}
loading={isResettingTwoFactor}
>
<Trans>Reset 2FA</Trans>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</Alert>
</div>
);
};

View File

@ -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();

View File

@ -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`),

View File

@ -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),

View File

@ -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);

View File

@ -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?.();
}, },

View File

@ -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,

View File

@ -91,6 +91,8 @@ 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 { mutateAsync: createDocumentFromDirectTemplate, isPending: isSubmitting } = const { mutateAsync: createDocumentFromDirectTemplate, isPending: isSubmitting } =
@ -442,7 +444,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>

View File

@ -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, () => (

View File

@ -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();
@ -465,7 +467,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>

View File

@ -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}

View File

@ -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: {

View File

@ -0,0 +1,138 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { Textarea } from '@documenso/ui/primitives/textarea';
import { useToast } from '@documenso/ui/primitives/use-toast';
const ZSupportTicketSchema = z.object({
subject: z.string().min(3, 'Subject is required'),
message: z.string().min(10, 'Message must be at least 10 characters'),
});
type TSupportTicket = z.infer<typeof ZSupportTicketSchema>;
export type SupportTicketFormProps = {
organisationId: string;
teamId?: string | null;
onSuccess?: () => void;
onClose?: () => void;
};
export const SupportTicketForm = ({
organisationId,
teamId,
onSuccess,
onClose,
}: SupportTicketFormProps) => {
const { t } = useLingui();
const { toast } = useToast();
const { mutateAsync: submitSupportTicket, isPending } =
trpc.profile.submitSupportTicket.useMutation();
const form = useForm<TSupportTicket>({
resolver: zodResolver(ZSupportTicketSchema),
defaultValues: {
subject: '',
message: '',
},
});
const isLoading = form.formState.isLoading || isPending;
const onSubmit = async (data: TSupportTicket) => {
const { subject, message } = data;
try {
await submitSupportTicket({
subject,
message,
organisationId,
teamId,
});
toast({
title: t`Support ticket created`,
description: t`Your support request has been submitted. We'll get back to you soon!`,
});
if (onSuccess) {
onSuccess();
}
form.reset();
} catch (err) {
toast({
title: t`Failed to create support ticket`,
description: t`An error occurred. Please try again later.`,
variant: 'destructive',
});
}
};
return (
<>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<fieldset disabled={isLoading} className="flex flex-col gap-4">
<FormField
control={form.control}
name="subject"
render={({ field }) => (
<FormItem>
<FormLabel required>
<Trans>Subject</Trans>
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormLabel required>
<Trans>Message</Trans>
</FormLabel>
<FormControl>
<Textarea rows={5} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="mt-2 flex flex-row gap-2">
<Button type="submit" size="sm" loading={isLoading}>
<Trans>Submit</Trans>
</Button>
{onClose && (
<Button variant="outline" size="sm" type="button" onClick={onClose}>
<Trans>Close</Trans>
</Button>
)}
</div>
</fieldset>
</form>
</Form>
</>
);
};

View File

@ -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);
}, },

View File

@ -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';

View File

@ -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,
}, },

View File

@ -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>

View File

@ -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);

View File

@ -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,
}, },

View File

@ -78,6 +78,8 @@ export const DocumentSigningPageView = ({
const targetSigner = const targetSigner =
recipient.role === RecipientRole.ASSISTANT && selectedSigner ? selectedSigner : null; recipient.role === RecipientRole.ASSISTANT && selectedSigner ? selectedSigner : null;
const highestPageNumber = Math.max(...fields.map((field) => field.page));
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">
@ -224,7 +226,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) =>

View File

@ -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 {

View File

@ -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;

View File

@ -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,
}, },
@ -218,15 +204,11 @@ export const DocumentEditForm = ({
const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => { const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => {
try { try {
await Promise.all([ await Promise.all([
setSigningOrderForDocument({
documentId: document.id,
signingOrder: data.signingOrder,
}),
updateDocument({ updateDocument({
documentId: document.id, documentId: document.id,
meta: { meta: {
allowDictateNextSigner: data.allowDictateNextSigner, allowDictateNextSigner: data.allowDictateNextSigner,
signingOrder: data.signingOrder,
}, },
}), }),

View File

@ -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,
}, },

View File

@ -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,
}, },

View File

@ -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,

View File

@ -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) {

View File

@ -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') {

View File

@ -321,6 +321,19 @@ export const OrgMenuSwitcher = () => {
<Trans>Language</Trans> <Trans>Language</Trans>
</DropdownMenuItem> </DropdownMenuItem>
{currentOrganisation && (
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
<Link
to={{
pathname: `/o/${currentOrganisation.url}/support`,
search: currentTeam ? `?team=${currentTeam.id}` : '',
}}
>
<Trans>Support</Trans>
</Link>
</DropdownMenuItem>
)}
<DropdownMenuItem <DropdownMenuItem
className="text-muted-foreground hover:!text-muted-foreground px-4 py-2" className="text-muted-foreground hover:!text-muted-foreground px-4 py-2"
onSelect={async () => authClient.signOut()} onSelect={async () => authClient.signOut()}

View File

@ -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,

View File

@ -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',

View File

@ -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 [

View File

@ -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,

View File

@ -46,7 +46,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,
}, },

View File

@ -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({

View File

@ -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';

View File

@ -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();

View File

@ -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`),

View File

@ -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,

View File

@ -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`),

View File

@ -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,

View File

@ -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,
@ -27,17 +27,18 @@ import { AdminOrganisationCreateDialog } from '~/components/dialogs/admin-organi
import { AdminUserDeleteDialog } from '~/components/dialogs/admin-user-delete-dialog'; import { AdminUserDeleteDialog } from '~/components/dialogs/admin-user-delete-dialog';
import { AdminUserDisableDialog } from '~/components/dialogs/admin-user-disable-dialog'; import { AdminUserDisableDialog } from '~/components/dialogs/admin-user-disable-dialog';
import { AdminUserEnableDialog } from '~/components/dialogs/admin-user-enable-dialog'; import { AdminUserEnableDialog } from '~/components/dialogs/admin-user-enable-dialog';
import { AdminUserResetTwoFactorDialog } from '~/components/dialogs/admin-user-reset-two-factor-dialog';
import { GenericErrorLayout } from '~/components/general/generic-error-layout'; import { GenericErrorLayout } from '~/components/general/generic-error-layout';
import { AdminOrganisationsTable } from '~/components/tables/admin-organisations-table'; import { AdminOrganisationsTable } from '~/components/tables/admin-organisations-table';
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),
}, },
@ -77,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),
@ -219,10 +220,11 @@ const AdminUserPage = ({ user }: { user: User }) => {
/> />
</div> </div>
<div className="mt-16 flex flex-col items-center gap-4"> <div className="mt-16 flex flex-col gap-4">
{user && <AdminUserDeleteDialog user={user} />} {user && user.twoFactorEnabled && <AdminUserResetTwoFactorDialog user={user} />}
{user && user.disabled && <AdminUserEnableDialog userToEnable={user} />} {user && user.disabled && <AdminUserEnableDialog userToEnable={user} />}
{user && !user.disabled && <AdminUserDisableDialog userToDisable={user} />} {user && !user.disabled && <AdminUserDisableDialog userToDisable={user} />}
{user && <AdminUserDeleteDialog user={user} />}
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,125 @@
import { useState } from 'react';
import { Trans } from '@lingui/react/macro';
import { BookIcon, HelpCircleIcon, Link2Icon } from 'lucide-react';
import { Link, useSearchParams } from 'react-router';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { Button } from '@documenso/ui/primitives/button';
import { SupportTicketForm } from '~/components/forms/support-ticket-form';
import { appMetaTags } from '~/utils/meta';
export function meta() {
return appMetaTags('Support');
}
export default function SupportPage() {
const [showForm, setShowForm] = useState(false);
const { user } = useSession();
const organisation = useCurrentOrganisation();
const [searchParams] = useSearchParams();
const teamId = searchParams.get('team');
const subscriptionStatus = organisation.subscription?.status;
const handleSuccess = () => {
setShowForm(false);
};
const handleCloseForm = () => {
setShowForm(false);
};
return (
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
<div className="mb-8">
<h1 className="flex flex-row items-center gap-2 text-3xl font-bold">
<HelpCircleIcon className="text-muted-foreground h-8 w-8" />
<Trans>Support</Trans>
</h1>
<p className="text-muted-foreground mt-2">
<Trans>Your current plan includes the following support channels:</Trans>
</p>
<div className="mt-6 flex flex-col gap-4">
<div className="rounded-lg border p-4">
<h2 className="flex items-center gap-2 text-lg font-bold">
<BookIcon className="text-muted-foreground h-5 w-5" />
<Link
to="https://docs.documenso.com"
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
<Trans>Documentation</Trans>
</Link>
</h2>
<p className="text-muted-foreground mt-1">
<Trans>Read our documentation to get started with Documenso.</Trans>
</p>
</div>
<div className="rounded-lg border p-4">
<h2 className="flex items-center gap-2 text-lg font-bold">
<Link2Icon className="text-muted-foreground h-5 w-5" />
<Link
to="https://documen.so/discord"
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
<Trans>Discord</Trans>
</Link>
</h2>
<p className="text-muted-foreground mt-1">
<Trans>
Join our community on{' '}
<Link
to="https://documen.so/discord"
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
Discord
</Link>{' '}
for community support and discussion.
</Trans>
</p>
</div>
{organisation && IS_BILLING_ENABLED() && subscriptionStatus && (
<>
<div className="rounded-lg border p-4">
<h2 className="flex items-center gap-2 text-lg font-bold">
<Link2Icon className="text-muted-foreground h-5 w-5" />
<Trans>Contact us</Trans>
</h2>
<p className="text-muted-foreground mt-1">
<Trans>We'll get back to you as soon as possible via email.</Trans>
</p>
<div className="mt-4">
{!showForm ? (
<Button variant="outline" size="sm" onClick={() => setShowForm(true)}>
<Trans>Create a support ticket</Trans>
</Button>
) : (
<SupportTicketForm
organisationId={organisation.id}
teamId={teamId}
onSuccess={handleSuccess}
onClose={handleCloseForm}
/>
)}
</div>
</div>
</>
)}
</div>
</div>
</div>
);
}

View File

@ -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) {

View File

@ -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';

View File

@ -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();

View File

@ -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.

View File

@ -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"
} }

View File

@ -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

60
package-lock.json generated
View File

@ -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": "*",
@ -3522,6 +3522,15 @@
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@graphql-typed-document-node/core": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
"integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
"license": "MIT",
"peerDependencies": {
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
"node_modules/@grpc/grpc-js": { "node_modules/@grpc/grpc-js": {
"version": "1.13.3", "version": "1.13.3",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.3.tgz", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.3.tgz",
@ -11826,6 +11835,20 @@
"url": "https://github.com/sponsors/tannerlinsley" "url": "https://github.com/sponsors/tannerlinsley"
} }
}, },
"node_modules/@team-plain/typescript-sdk": {
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/@team-plain/typescript-sdk/-/typescript-sdk-5.9.0.tgz",
"integrity": "sha512-AHSXyt1kDt74m9YKZBCRCd6cQjB8QjUNr9cehtR2QHzZ/8yXJPzawPJDqOQ3ms5KvwuYrBx2qT3e6C/zrQ5UtA==",
"license": "MIT",
"dependencies": {
"@graphql-typed-document-node/core": "^3.2.0",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"graphql": "^16.6.0",
"lodash.get": "^4.4.2",
"zod": "3.22.4"
}
},
"node_modules/@theguild/remark-mermaid": { "node_modules/@theguild/remark-mermaid": {
"version": "0.0.5", "version": "0.0.5",
"resolved": "https://registry.npmjs.org/@theguild/remark-mermaid/-/remark-mermaid-0.0.5.tgz", "resolved": "https://registry.npmjs.org/@theguild/remark-mermaid/-/remark-mermaid-0.0.5.tgz",
@ -13235,7 +13258,6 @@
"version": "8.17.1", "version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
@ -13248,6 +13270,23 @@
"url": "https://github.com/sponsors/epoberezkin" "url": "https://github.com/sponsors/epoberezkin"
} }
}, },
"node_modules/ajv-formats": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/ansi-escapes": { "node_modules/ansi-escapes": {
"version": "4.3.2", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@ -18771,7 +18810,6 @@
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -19847,6 +19885,15 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/graphql": {
"version": "16.11.0",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz",
"integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==",
"license": "MIT",
"engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
}
},
"node_modules/gray-matter": { "node_modules/gray-matter": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
@ -22329,7 +22376,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/json-stable-stringify-without-jsonify": { "node_modules/json-stable-stringify-without-jsonify": {
@ -30570,7 +30616,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@ -36583,6 +36628,7 @@
"@pdf-lib/fontkit": "^1.1.1", "@pdf-lib/fontkit": "^1.1.1",
"@scure/base": "^1.1.3", "@scure/base": "^1.1.3",
"@sindresorhus/slugify": "^2.2.1", "@sindresorhus/slugify": "^2.2.1",
"@team-plain/typescript-sdk": "^5.9.0",
"@vvo/tzdb": "^6.117.0", "@vvo/tzdb": "^6.117.0",
"csv-parse": "^5.6.0", "csv-parse": "^5.6.0",
"inngest": "^3.19.13", "inngest": "^3.19.13",

View File

@ -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",

View File

@ -330,6 +330,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 +737,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,

View File

@ -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({

View File

@ -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.

View File

@ -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: {

View File

@ -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,
},
},
}, },
}); });

View File

@ -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({

View File

@ -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,
}, },
}); });

View File

@ -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: {

View File

@ -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: {

View File

@ -33,6 +33,7 @@
"@pdf-lib/fontkit": "^1.1.1", "@pdf-lib/fontkit": "^1.1.1",
"@scure/base": "^1.1.3", "@scure/base": "^1.1.3",
"@sindresorhus/slugify": "^2.2.1", "@sindresorhus/slugify": "^2.2.1",
"@team-plain/typescript-sdk": "^5.9.0",
"@vvo/tzdb": "^6.117.0", "@vvo/tzdb": "^6.117.0",
"csv-parse": "^5.6.0", "csv-parse": "^5.6.0",
"inngest": "^3.19.13", "inngest": "^3.19.13",

View File

@ -0,0 +1,7 @@
import { PlainClient } from '@team-plain/typescript-sdk';
import { env } from '@documenso/lib/utils/env';
export const plainClient = new PlainClient({
apiKey: env('NEXT_PRIVATE_PLAIN_API_KEY') ?? '',
});

View File

@ -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,
}, },

View File

@ -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';
@ -45,7 +46,8 @@ export type CreateDocumentOptions = {
globalAccessAuth?: TDocumentAccessAuthTypes[]; globalAccessAuth?: TDocumentAccessAuthTypes[];
globalActionAuth?: TDocumentActionAuthTypes[]; globalActionAuth?: TDocumentActionAuthTypes[];
formValues?: TDocumentFormValues; formValues?: TDocumentFormValues;
recipients: TCreateDocumentV2Request['recipients']; recipients: TCreateDocumentTemporaryRequest['recipients'];
folderId?: string;
expiryAmount?: number; expiryAmount?: number;
expiryUnit?: string; expiryUnit?: string;
}; };
@ -62,7 +64,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 }),
@ -81,6 +83,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,
@ -167,6 +185,7 @@ export const createDocumentV2 = async ({
teamId, teamId,
authOptions, authOptions,
visibility, visibility,
folderId,
formValues, formValues,
source: DocumentSource.DOCUMENT, source: DocumentSource.DOCUMENT,
documentMeta: { documentMeta: {

View File

@ -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)

View File

@ -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,
},
}; };
}; };

View File

@ -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,

View File

@ -28,13 +28,7 @@ export async function rejectDocumentWithToken({
documentId, documentId,
}, },
include: { include: {
document: { document: true,
include: {
user: true,
recipients: true,
documentMeta: true,
},
},
}, },
}); });

View File

@ -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,

View File

@ -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,
}, },
}); });

View File

@ -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,
},
},
}, },
}); });

View File

@ -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);

View File

@ -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';

View 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;
};

View File

@ -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.

View File

@ -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.

View File

@ -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,

View File

@ -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,
},
},
}, },
}); });

View File

@ -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,
@ -70,6 +71,7 @@ export type CreateDocumentFromTemplateOptions = {
email: string; email: string;
signingOrder?: number | null; signingOrder?: number | null;
}[]; }[];
folderId?: string;
prefillFields?: TFieldMetaPrefillFieldsSchema[]; prefillFields?: TFieldMetaPrefillFieldsSchema[];
customDocumentDataId?: string; customDocumentDataId?: string;
@ -277,6 +279,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({
@ -301,6 +304,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,
@ -371,6 +390,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,

View File

@ -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;
}; };

View File

@ -24,7 +24,14 @@ export const resetPassword = async ({ token, password, requestMetadata }: ResetP
token, token,
}, },
include: { include: {
user: true, user: {
select: {
id: true,
email: true,
name: true,
password: true,
},
},
}, },
}); });

View File

@ -0,0 +1,72 @@
import { plainClient } from '@documenso/lib/plain/client';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { buildOrganisationWhereQuery } from '../../utils/organisations';
import { getTeamById } from '../team/get-team';
type SubmitSupportTicketOptions = {
subject: string;
message: string;
userId: number;
organisationId: string;
teamId?: number | null;
};
export const submitSupportTicket = async ({
subject,
message,
userId,
organisationId,
teamId,
}: SubmitSupportTicketOptions) => {
const user = await prisma.user.findFirst({
where: {
id: userId,
},
});
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
});
}
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery({
organisationId,
userId,
}),
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Organisation not found',
});
}
const team = teamId
? await getTeamById({
userId,
teamId,
})
: null;
const customMessage = `
Organisation: ${organisation.name} (${organisation.id})
Team: ${team ? `${team.name} (${team.id})` : 'No team provided'}
${message}`;
const res = await plainClient.createThread({
title: subject,
customerIdentifier: { emailAddress: user.email },
components: [{ componentText: { text: customMessage } }],
});
if (res.error) {
throw new Error(res.error.message);
}
return res;
};

View File

@ -24,7 +24,7 @@ export const updateProfile = async ({
}, },
}); });
return await prisma.$transaction(async (tx) => { await prisma.$transaction(async (tx) => {
await tx.userSecurityAuditLog.create({ await tx.userSecurityAuditLog.create({
data: { data: {
userId, userId,
@ -34,7 +34,7 @@ export const updateProfile = async ({
}, },
}); });
return await tx.user.update({ await tx.user.update({
where: { where: {
id: userId, id: userId,
}, },

View File

@ -12,7 +12,13 @@ export type VerifyEmailProps = {
export const verifyEmail = async ({ token }: VerifyEmailProps) => { export const verifyEmail = async ({ token }: VerifyEmailProps) => {
const verificationToken = await prisma.verificationToken.findFirst({ const verificationToken = await prisma.verificationToken.findFirst({
include: { include: {
user: true, user: {
select: {
id: true,
email: true,
name: true,
},
},
}, },
where: { where: {
token, token,

View File

@ -4,7 +4,7 @@ import type { DocumentWithRecipients } from '@documenso/prisma/types/document-wi
export type MaskRecipientTokensForDocumentOptions<T extends DocumentWithRecipients> = { export type MaskRecipientTokensForDocumentOptions<T extends DocumentWithRecipients> = {
document: T; document: T;
user?: User; user?: Pick<User, 'id' | 'email' | 'name'>;
token?: string; token?: string;
}; };

View File

@ -0,0 +1,28 @@
import { sendDeleteEmail } from '@documenso/lib/server-only/document/send-delete-email';
import { superDeleteDocument } from '@documenso/lib/server-only/document/super-delete-document';
import { adminProcedure } from '../trpc';
import {
ZDeleteDocumentRequestSchema,
ZDeleteDocumentResponseSchema,
} from './delete-document.types';
export const deleteDocumentRoute = adminProcedure
.input(ZDeleteDocumentRequestSchema)
.output(ZDeleteDocumentResponseSchema)
.mutation(async ({ ctx, input }) => {
const { id, reason } = input;
ctx.logger.info({
input: {
id,
},
});
await sendDeleteEmail({ documentId: id, reason });
await superDeleteDocument({
id,
requestMetadata: ctx.metadata.requestMetadata,
});
});

View File

@ -0,0 +1,11 @@
import { z } from 'zod';
export const ZDeleteDocumentRequestSchema = z.object({
id: z.number().min(1),
reason: z.string(),
});
export const ZDeleteDocumentResponseSchema = z.void();
export type TDeleteDocumentRequest = z.infer<typeof ZDeleteDocumentRequestSchema>;
export type TDeleteDocumentResponse = z.infer<typeof ZDeleteDocumentResponseSchema>;

View File

@ -0,0 +1,19 @@
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
import { adminProcedure } from '../trpc';
import { ZDeleteUserRequestSchema, ZDeleteUserResponseSchema } from './delete-user.types';
export const deleteUserRoute = adminProcedure
.input(ZDeleteUserRequestSchema)
.output(ZDeleteUserResponseSchema)
.mutation(async ({ input, ctx }) => {
const { id } = input;
ctx.logger.info({
input: {
id,
},
});
await deleteUser({ id });
});

View File

@ -0,0 +1,10 @@
import { z } from 'zod';
export const ZDeleteUserRequestSchema = z.object({
id: z.number().min(1),
});
export const ZDeleteUserResponseSchema = z.void();
export type TDeleteUserRequest = z.infer<typeof ZDeleteUserRequestSchema>;
export type TDeleteUserResponse = z.infer<typeof ZDeleteUserResponseSchema>;

View File

@ -0,0 +1,29 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { disableUser } from '@documenso/lib/server-only/user/disable-user';
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
import { adminProcedure } from '../trpc';
import { ZDisableUserRequestSchema, ZDisableUserResponseSchema } from './disable-user.types';
export const disableUserRoute = adminProcedure
.input(ZDisableUserRequestSchema)
.output(ZDisableUserResponseSchema)
.mutation(async ({ input, ctx }) => {
const { id } = input;
ctx.logger.info({
input: {
id,
},
});
const user = await getUserById({ id }).catch(() => null);
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
});
}
await disableUser({ id });
});

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