Compare commits

..

9 Commits

Author SHA1 Message Date
Ephraim Atta-Duncan
e7770a184d feat: migrate webhook to background job 2025-03-11 01:00:54 +00:00
David Nguyen
c646afcd97 fix: tests 2025-03-09 15:10:19 +11:00
Catalin Pit
63d990ce8d fix: optional fields in embeds (#1691) 2025-03-09 14:41:17 +11:00
eddielu
aa7d6b28a4 docs: Update documentation to match reality. colorPrimary, colorBackground,… (#1666)
Update documentation to match reality. colorPrimary, colorBackground,
and borderRadius do not exist according to the schema:
280251cfdd/packages/react/src/css-vars.ts
2025-03-09 14:38:51 +11:00
David Nguyen
b990532633 fix: remove refresh on focus 2025-03-08 15:30:13 +11:00
Catalin Pit
65be37514f fix: prefill fields (#1689)
Users can now selectively choose which properties to pre-fill for each
field - from just a label to all available properties.
2025-03-07 09:09:15 +11:00
Catalin Pit
0df29fce36 fix: invalid request body (#1686)
Fix the invalid request body so the webhooks work again.
2025-03-06 19:47:24 +11:00
Ephraim Duncan
ba5b7ce480 feat: hide signature ui when theres no signature field (#1676) 2025-03-06 19:47:02 +11:00
Catalin Pit
422770a8c7 feat: allow fields prefill when generating a document from a template (#1615)
This change allows API users to pre-fill fields with values by
passing the data in the request body. Example body for V2 API endpoint
`/api/v2-beta/template/use`:

```json
{
    "templateId": 1,
    "recipients": [
        {
            "id": 1,
            "email": "signer1@mail.com",
            "name": "Signer 1"
        },
        {
            "id": 2,
            "email": "signer2@mail.com",
            "name": "Signer 2"
        }
    ],
    "prefillValues": [
        {
            "id": 14,
            "fieldMeta": {
                "type": "text",
                "label": "my label",
                "placeholder": "text placeholder test",
                "text": "auto-sign value",
                "characterLimit": 25,
                "textAlign": "right",
                "fontSize": 94,
                "required": true
            }
        },
        {
            "id": 15,
            "fieldMeta": {
                "type": "radio",
                "label": "radio label",
                "placeholder": "new radio placeholder",
                "required": false,
                "readOnly": true,
                "values": [
                    {
                        "id": 2,
                        "checked": true,
                        "value": "radio val 1"
                    },
                    {
                        "id": 3,
                        "checked": false,
                        "value": "radio val 2"
                    }
                ]
            }
        },
        {
            "id": 16,
            "fieldMeta": {
                "type": "dropdown",
                "label": "dropdown label",
                "placeholder": "DD placeholder",
                "required": false,
                "readOnly": false,
                "values": [
                    {
                        "value": "option 1"
                    },
                    {
                        "value": "option 2"
                    },
                    {
                        "value": "option 3"
                    }
                ],
                "defaultValue": "option 2"
            }
        }
    ],
    "distributeDocument": false,
    "customDocumentDataId": ""
}
```
2025-03-06 19:45:33 +11:00
44 changed files with 2033 additions and 333 deletions

View File

@@ -52,9 +52,9 @@ Platform customers have access to advanced styling options to customize the embe
<EmbedDirectTemplate <EmbedDirectTemplate
token={token} token={token}
cssVars={{ cssVars={{
colorPrimary: '#0000FF', primary: '#0000FF',
colorBackground: '#F5F5F5', background: '#F5F5F5',
borderRadius: '8px', radius: '8px',
}} }}
/> />
``` ```

View File

@@ -95,9 +95,9 @@ const MyEmbeddingComponent = () => {
} }
`; `;
const cssVars = { const cssVars = {
colorPrimary: '#0000FF', primary: '#0000FF',
colorBackground: '#F5F5F5', background: '#F5F5F5',
borderRadius: '8px', radius: '8px',
}; };
return ( return (

View File

@@ -99,9 +99,9 @@ const MyEmbeddingComponent = () => {
`} `}
// CSS Variables // CSS Variables
cssVars={{ cssVars={{
colorPrimary: '#0000FF', primary: '#0000FF',
colorBackground: '#F5F5F5', background: '#F5F5F5',
borderRadius: '8px', radius: '8px',
}} }}
// Dark Mode Control // Dark Mode Control
darkModeDisabled={true} darkModeDisabled={true}

View File

@@ -95,9 +95,9 @@ const MyEmbeddingComponent = () => {
} }
`; `;
const cssVars = { const cssVars = {
colorPrimary: '#0000FF', primary: '#0000FF',
colorBackground: '#F5F5F5', background: '#F5F5F5',
borderRadius: '8px', radius: '8px',
}; };
return ( return (

View File

@@ -97,9 +97,9 @@ Platform customers have access to advanced styling options:
} }
`; `;
const cssVars = { const cssVars = {
colorPrimary: '#0000FF', primary: '#0000FF',
colorBackground: '#F5F5F5', background: '#F5F5F5',
borderRadius: '8px', radius: '8px',
}; };
</script> </script>

View File

@@ -97,9 +97,9 @@ Platform customers have access to advanced styling options:
} }
`; `;
const cssVars = { const cssVars = {
colorPrimary: '#0000FF', primary: '#0000FF',
colorBackground: '#F5F5F5', background: '#F5F5F5',
borderRadius: '8px', radius: '8px',
}; };
</script> </script>

View File

@@ -13,6 +13,10 @@ import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import {
isFieldUnsignedAndRequired,
isRequiredField,
} from '@documenso/lib/utils/advanced-fields-helpers';
import { validateFieldsInserted } from '@documenso/lib/utils/fields'; import { validateFieldsInserted } from '@documenso/lib/utils/fields';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { import type {
@@ -92,7 +96,7 @@ export const EmbedDirectTemplateClientPage = ({
const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(() => fields); const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(() => fields);
const [pendingFields, _completedFields] = [ const [pendingFields, _completedFields] = [
localFields.filter((field) => !field.inserted), localFields.filter((field) => isFieldUnsignedAndRequired(field)),
localFields.filter((field) => field.inserted), localFields.filter((field) => field.inserted),
]; ];
@@ -110,7 +114,7 @@ export const EmbedDirectTemplateClientPage = ({
const newField: DirectTemplateLocalField = structuredClone({ const newField: DirectTemplateLocalField = structuredClone({
...field, ...field,
customText: payload.value, customText: payload.value ?? '',
inserted: true, inserted: true,
signedValue: payload, signedValue: payload,
}); });
@@ -121,8 +125,10 @@ export const EmbedDirectTemplateClientPage = ({
created: new Date(), created: new Date(),
recipientId: 1, recipientId: 1,
fieldId: 1, fieldId: 1,
signatureImageAsBase64: payload.value.startsWith('data:') ? payload.value : null, signatureImageAsBase64:
typedSignature: payload.value.startsWith('data:') ? null : payload.value, payload.value && payload.value.startsWith('data:') ? payload.value : null,
typedSignature:
payload.value && !payload.value.startsWith('data:') ? payload.value : null,
} satisfies Signature; } satisfies Signature;
} }
@@ -180,7 +186,7 @@ export const EmbedDirectTemplateClientPage = ({
}; };
const onNextFieldClick = () => { const onNextFieldClick = () => {
validateFieldsInserted(localFields); validateFieldsInserted(pendingFields);
setShowPendingFieldTooltip(true); setShowPendingFieldTooltip(true);
setIsExpanded(false); setIsExpanded(false);
@@ -192,7 +198,7 @@ export const EmbedDirectTemplateClientPage = ({
return; return;
} }
const valid = validateFieldsInserted(localFields); const valid = validateFieldsInserted(pendingFields);
if (!valid) { if (!valid) {
setShowPendingFieldTooltip(true); setShowPendingFieldTooltip(true);
@@ -205,12 +211,6 @@ export const EmbedDirectTemplateClientPage = ({
directTemplateExternalId = decodeURIComponent(directTemplateExternalId); directTemplateExternalId = decodeURIComponent(directTemplateExternalId);
} }
localFields.forEach((field) => {
if (!field.signedValue) {
throw new Error('Invalid configuration');
}
});
const { const {
documentId, documentId,
token: documentToken, token: documentToken,
@@ -221,13 +221,11 @@ export const EmbedDirectTemplateClientPage = ({
directRecipientName: fullName, directRecipientName: fullName,
directRecipientEmail: email, directRecipientEmail: email,
templateUpdatedAt: updatedAt, templateUpdatedAt: updatedAt,
signedFieldValues: localFields.map((field) => { signedFieldValues: localFields
if (!field.signedValue) { .filter((field) => {
throw new Error('Invalid configuration'); return field.signedValue && (isRequiredField(field) || field.inserted);
} })
.map((field) => field.signedValue!),
return field.signedValue;
}),
}); });
if (window.parent) { if (window.parent) {
@@ -415,6 +413,7 @@ export const EmbedDirectTemplateClientPage = ({
/> />
</div> </div>
{hasSignatureField && (
<div> <div>
<Label htmlFor="Signature"> <Label htmlFor="Signature">
<Trans>Signature</Trans> <Trans>Signature</Trans>
@@ -449,6 +448,7 @@ export const EmbedDirectTemplateClientPage = ({
</div> </div>
)} )}
</div> </div>
)}
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
import { useEffect, useId, useLayoutEffect, useState } from 'react'; import { useEffect, useId, useLayoutEffect, useMemo, 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';
@@ -15,6 +15,7 @@ import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'; import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
import { validateFieldsInserted } from '@documenso/lib/utils/fields'; import { validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields'; import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
@@ -101,19 +102,26 @@ export const EmbedSignDocumentClientPage = ({
const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500); const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500);
const [pendingFields, _completedFields] = [ const [pendingFields, _completedFields] = [
fields.filter((field) => field.recipientId === recipient.id && !field.inserted), fields.filter(
(field) => field.recipientId === recipient.id && isFieldUnsignedAndRequired(field),
),
fields.filter((field) => field.inserted), fields.filter((field) => field.inserted),
]; ];
const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } = const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } =
trpc.recipient.completeDocumentWithToken.useMutation(); trpc.recipient.completeDocumentWithToken.useMutation();
const fieldsRequiringValidation = useMemo(
() => fields.filter(isFieldUnsignedAndRequired),
[fields],
);
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE); const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
const assistantSignersId = useId(); const assistantSignersId = useId();
const onNextFieldClick = () => { const onNextFieldClick = () => {
validateFieldsInserted(fields); validateFieldsInserted(fieldsRequiringValidation);
setShowPendingFieldTooltip(true); setShowPendingFieldTooltip(true);
setIsExpanded(false); setIsExpanded(false);
@@ -125,7 +133,7 @@ export const EmbedSignDocumentClientPage = ({
return; return;
} }
const valid = validateFieldsInserted(fields); const valid = validateFieldsInserted(fieldsRequiringValidation);
if (!valid) { if (!valid) {
setShowPendingFieldTooltip(true); setShowPendingFieldTooltip(true);
@@ -418,6 +426,7 @@ export const EmbedSignDocumentClientPage = ({
/> />
</div> </div>
{hasSignatureField && (
<div> <div>
<Label htmlFor="Signature"> <Label htmlFor="Signature">
<Trans>Signature</Trans> <Trans>Signature</Trans>
@@ -452,6 +461,7 @@ export const EmbedSignDocumentClientPage = ({
</div> </div>
)} )}
</div> </div>
)}
</> </>
)} )}
</div> </div>

View File

@@ -91,7 +91,7 @@ export const DirectTemplateSigningForm = ({
const tempField: DirectTemplateLocalField = { const tempField: DirectTemplateLocalField = {
...field, ...field,
customText: value.value, customText: value.value ?? '',
inserted: true, inserted: true,
signedValue: value, signedValue: value,
}; };
@@ -102,8 +102,8 @@ export const DirectTemplateSigningForm = ({
created: new Date(), created: new Date(),
recipientId: 1, recipientId: 1,
fieldId: 1, fieldId: 1,
signatureImageAsBase64: value.value.startsWith('data:') ? value.value : null, signatureImageAsBase64: value.value?.startsWith('data:') ? value.value : null,
typedSignature: value.value.startsWith('data:') ? null : value.value, typedSignature: value.value && !value.value.startsWith('data:') ? value.value : null,
} satisfies Signature; } satisfies Signature;
} }

View File

@@ -3,6 +3,7 @@ import { useMemo, useState } from 'react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import type { Field } from '@prisma/client'; import type { Field } from '@prisma/client';
import { RecipientRole } from '@prisma/client'; import { RecipientRole } from '@prisma/client';
import { match } from 'ts-pattern';
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers'; import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@@ -58,21 +59,33 @@ export const DocumentSigningCompleteDialog = ({
loading={isSubmitting} loading={isSubmitting}
disabled={disabled} disabled={disabled}
> >
{isComplete ? <Trans>Complete</Trans> : <Trans>Next field</Trans>} {match({ isComplete, role })
.with({ isComplete: false }, () => <Trans>Next field</Trans>)
.with({ isComplete: true, role: RecipientRole.APPROVER }, () => <Trans>Approve</Trans>)
.with({ isComplete: true, role: RecipientRole.VIEWER }, () => (
<Trans>Mark as viewed</Trans>
))
.with({ isComplete: true }, () => <Trans>Complete</Trans>)
.exhaustive()}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogTitle> <DialogTitle>
<div className="text-foreground text-xl font-semibold"> <div className="text-foreground text-xl font-semibold">
{role === RecipientRole.VIEWER && <Trans>Complete Viewing</Trans>} {match(role)
{role === RecipientRole.SIGNER && <Trans>Complete Signing</Trans>} .with(RecipientRole.VIEWER, () => <Trans>Complete Viewing</Trans>)
{role === RecipientRole.APPROVER && <Trans>Complete Approval</Trans>} .with(RecipientRole.SIGNER, () => <Trans>Complete Signing</Trans>)
.with(RecipientRole.APPROVER, () => <Trans>Complete Approval</Trans>)
.with(RecipientRole.CC, () => <Trans>Complete Viewing</Trans>)
.with(RecipientRole.ASSISTANT, () => <Trans>Complete Assisting</Trans>)
.exhaustive()}
</div> </div>
</DialogTitle> </DialogTitle>
<div className="text-muted-foreground max-w-[50ch]"> <div className="text-muted-foreground max-w-[50ch]">
{role === RecipientRole.VIEWER && ( {match(role)
.with(RecipientRole.VIEWER, () => (
<span> <span>
<Trans> <Trans>
<span className="inline-flex flex-wrap"> <span className="inline-flex flex-wrap">
@@ -85,8 +98,8 @@ export const DocumentSigningCompleteDialog = ({
<br /> Are you sure? <br /> Are you sure?
</Trans> </Trans>
</span> </span>
)} ))
{role === RecipientRole.SIGNER && ( .with(RecipientRole.SIGNER, () => (
<span> <span>
<Trans> <Trans>
<span className="inline-flex flex-wrap"> <span className="inline-flex flex-wrap">
@@ -99,8 +112,8 @@ export const DocumentSigningCompleteDialog = ({
<br /> Are you sure? <br /> Are you sure?
</Trans> </Trans>
</span> </span>
)} ))
{role === RecipientRole.APPROVER && ( .with(RecipientRole.APPROVER, () => (
<span> <span>
<Trans> <Trans>
<span className="inline-flex flex-wrap"> <span className="inline-flex flex-wrap">
@@ -113,7 +126,21 @@ export const DocumentSigningCompleteDialog = ({
<br /> Are you sure? <br /> Are you sure?
</Trans> </Trans>
</span> </span>
)} ))
.otherwise(() => (
<span>
<Trans>
<span className="inline-flex flex-wrap">
You are about to complete viewing "
<span className="inline-block max-w-[11rem] truncate align-baseline">
{documentTitle}
</span>
".
</span>
<br /> Are you sure?
</Trans>
</span>
))}
</div> </div>
<DocumentSigningDisclosure className="mt-4" /> <DocumentSigningDisclosure className="mt-4" />
@@ -138,9 +165,13 @@ export const DocumentSigningCompleteDialog = ({
loading={isSubmitting} loading={isSubmitting}
onClick={onSignatureComplete} onClick={onSignatureComplete}
> >
{role === RecipientRole.VIEWER && <Trans>Mark as Viewed</Trans>} {match(role)
{role === RecipientRole.SIGNER && <Trans>Sign</Trans>} .with(RecipientRole.VIEWER, () => <Trans>Mark as Viewed</Trans>)
{role === RecipientRole.APPROVER && <Trans>Approve</Trans>} .with(RecipientRole.SIGNER, () => <Trans>Sign</Trans>)
.with(RecipientRole.APPROVER, () => <Trans>Approve</Trans>)
.with(RecipientRole.CC, () => <Trans>Mark as Viewed</Trans>)
.with(RecipientRole.ASSISTANT, () => <Trans>Complete</Trans>)
.exhaustive()}
</Button> </Button>
</div> </div>
</DialogFooter> </DialogFooter>

View File

@@ -313,7 +313,11 @@ export const DocumentSigningForm = ({
<> <>
<form onSubmit={handleSubmit(onFormSubmit)}> <form onSubmit={handleSubmit(onFormSubmit)}>
<p className="text-muted-foreground mt-2 text-sm"> <p className="text-muted-foreground mt-2 text-sm">
{recipient.role === RecipientRole.APPROVER && !hasSignatureField ? (
<Trans>Please review the document before approving.</Trans>
) : (
<Trans>Please review the document before signing.</Trans> <Trans>Please review the document before signing.</Trans>
)}
</p> </p>
<hr className="border-border mb-8 mt-4" /> <hr className="border-border mb-8 mt-4" />
@@ -337,6 +341,7 @@ export const DocumentSigningForm = ({
/> />
</div> </div>
{hasSignatureField && (
<div> <div>
<Label htmlFor="Signature"> <Label htmlFor="Signature">
<Trans>Signature</Trans> <Trans>Signature</Trans>
@@ -361,7 +366,7 @@ export const DocumentSigningForm = ({
</CardContent> </CardContent>
</Card> </Card>
{hasSignatureField && !signatureValid && ( {!signatureValid && (
<div className="text-destructive mt-2 text-sm"> <div className="text-destructive mt-2 text-sm">
<Trans> <Trans>
Signature is too small. Please provide a more complete signature. Signature is too small. Please provide a more complete signature.
@@ -369,6 +374,7 @@ export const DocumentSigningForm = ({
</div> </div>
)} )}
</div> </div>
)}
</div> </div>
<div className="flex flex-col gap-4 md:flex-row"> <div className="flex flex-col gap-4 md:flex-row">

View File

@@ -2,6 +2,9 @@ import { useCallback, useEffect } from 'react';
import { useRevalidator } from 'react-router'; import { useRevalidator } from 'react-router';
/**
* Not really used anymore, this causes random 500s when the user refreshes while this occurs.
*/
export const RefreshOnFocus = () => { export const RefreshOnFocus = () => {
const { revalidate, state } = useRevalidator(); const { revalidate, state } = useRevalidator();

View File

@@ -27,7 +27,6 @@ import { TooltipProvider } from '@documenso/ui/primitives/tooltip';
import type { Route } from './+types/root'; import type { Route } from './+types/root';
import stylesheet from './app.css?url'; import stylesheet from './app.css?url';
import { GenericErrorLayout } from './components/general/generic-error-layout'; import { GenericErrorLayout } from './components/general/generic-error-layout';
import { RefreshOnFocus } from './components/general/refresh-on-focus';
import { langCookie } from './storage/lang-cookie.server'; import { langCookie } from './storage/lang-cookie.server';
import { themeSessionResolver } from './storage/theme-session.server'; import { themeSessionResolver } from './storage/theme-session.server';
import { appMetaTags } from './utils/meta'; import { appMetaTags } from './utils/meta';
@@ -159,8 +158,6 @@ export function LayoutContent({ children }: { children: React.ReactNode }) {
<ScrollRestoration /> <ScrollRestoration />
<Scripts /> <Scripts />
<RefreshOnFocus />
<script <script
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: `window.__ENV__ = ${JSON.stringify(publicEnv)}`, __html: `window.__ENV__ = ${JSON.stringify(publicEnv)}`,

View File

@@ -4,7 +4,7 @@ import { msg } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { PlusIcon } from 'lucide-react'; import { PlusIcon } from 'lucide-react';
import { ChevronLeft } from 'lucide-react'; import { ChevronLeft } from 'lucide-react';
import { Link, Outlet } from 'react-router'; import { Link, Outlet, isRouteErrorResponse } from 'react-router';
import LogoIcon from '@documenso/assets/logo_icon.png'; import LogoIcon from '@documenso/assets/logo_icon.png';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session'; import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
@@ -16,6 +16,8 @@ import { BrandingLogo } from '~/components/general/branding-logo';
import { GenericErrorLayout } from '~/components/general/generic-error-layout'; import { GenericErrorLayout } from '~/components/general/generic-error-layout';
import { appMetaTags } from '~/utils/meta'; import { appMetaTags } from '~/utils/meta';
import type { Route } from './+types/_layout';
export function meta() { export function meta() {
return appMetaTags('Profile'); return appMetaTags('Profile');
} }
@@ -96,7 +98,9 @@ export default function PublicProfileLayout() {
); );
} }
export function ErrorBoundary() { export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
const errorCode = isRouteErrorResponse(error) ? error.status : 500;
const errorCodeMap = { const errorCodeMap = {
404: { 404: {
subHeading: msg`404 Profile not found`, subHeading: msg`404 Profile not found`,
@@ -107,6 +111,7 @@ export function ErrorBoundary() {
return ( return (
<GenericErrorLayout <GenericErrorLayout
errorCode={errorCode}
errorCodeMap={errorCodeMap} errorCodeMap={errorCodeMap}
secondaryButton={null} secondaryButton={null}
primaryButton={ primaryButton={

View File

@@ -1,6 +1,6 @@
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { ChevronLeft } from 'lucide-react'; import { ChevronLeft } from 'lucide-react';
import { Link, Outlet } from 'react-router'; import { Link, Outlet, isRouteErrorResponse } from 'react-router';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session'; import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@@ -8,6 +8,8 @@ import { Button } from '@documenso/ui/primitives/button';
import { Header as AuthenticatedHeader } from '~/components/general/app-header'; import { Header as AuthenticatedHeader } from '~/components/general/app-header';
import { GenericErrorLayout } from '~/components/general/generic-error-layout'; import { GenericErrorLayout } from '~/components/general/generic-error-layout';
import type { Route } from './+types/_layout';
/** /**
* A layout to handle scenarios where the user is a recipient of a given resource * A layout to handle scenarios where the user is a recipient of a given resource
* where we do not care whether they are authenticated or not. * where we do not care whether they are authenticated or not.
@@ -30,9 +32,12 @@ export default function RecipientLayout() {
); );
} }
export function ErrorBoundary() { export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
const errorCode = isRouteErrorResponse(error) ? error.status : 500;
return ( return (
<GenericErrorLayout <GenericErrorLayout
errorCode={errorCode}
secondaryButton={null} secondaryButton={null}
primaryButton={ primaryButton={
<Button asChild className="w-32"> <Button asChild className="w-32">

View File

@@ -580,6 +580,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
userId: user.id, userId: user.id,
teamId: team?.id, teamId: team?.id,
recipients: body.recipients, recipients: body.recipients,
prefillFields: body.prefillFields,
override: { override: {
title: body.title, title: body.title,
...body.meta, ...body.meta,

View File

@@ -23,7 +23,7 @@ import {
ZRecipientActionAuthTypesSchema, ZRecipientActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth'; } from '@documenso/lib/types/document-auth';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta'; import { ZFieldMetaPrefillFieldsSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
extendZodWithOpenApi(z); extendZodWithOpenApi(z);
@@ -299,6 +299,7 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
}) })
.optional(), .optional(),
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(), formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
prefillFields: z.array(ZFieldMetaPrefillFieldsSchema).optional(),
}); });
export type TGenerateDocumentFromTemplateMutationSchema = z.infer< export type TGenerateDocumentFromTemplateMutationSchema = z.infer<

View File

@@ -0,0 +1,614 @@
import { expect, test } from '@playwright/test';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import type { TCheckboxFieldMeta, TRadioFieldMeta } from '@documenso/lib/types/field-meta';
import { prisma } from '@documenso/prisma';
import { FieldType, RecipientRole } from '@documenso/prisma/client';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import { apiSignin } from '../../fixtures/authentication';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
test.describe('Template Field Prefill API v1', () => {
test('should create a document from template with prefilled fields', async ({
page,
request,
}) => {
// 1. Create a user
const user = await seedUser();
// 2. Create an API token for the user
const { token } = await createApiToken({
userId: user.id,
tokenName: 'test-token',
expiresIn: null,
});
// 3. Create a template with seedBlankTemplate
const template = await seedBlankTemplate(user, {
createTemplateOptions: {
title: 'Template with Advanced Fields',
},
});
// 4. Create a recipient for the template
const recipient = await prisma.recipient.create({
data: {
templateId: template.id,
email: 'recipient@example.com',
name: 'Test Recipient',
role: RecipientRole.SIGNER,
token: 'test-token',
readStatus: 'NOT_OPENED',
sendStatus: 'NOT_SENT',
signingStatus: 'NOT_SIGNED',
},
});
// 5. Add fields to the template
// Add TEXT field
const textField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.TEXT,
page: 1,
positionX: 5,
positionY: 5,
width: 10,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'text',
label: 'Text Field',
},
},
});
// Add NUMBER field
const numberField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.NUMBER,
page: 1,
positionX: 5,
positionY: 15,
width: 10,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'number',
label: 'Number Field',
},
},
});
// Add RADIO field
const radioField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.RADIO,
page: 1,
positionX: 5,
positionY: 25,
width: 10,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'radio',
label: 'Radio Field',
values: [
{ id: 1, value: 'Option A', checked: false },
{ id: 2, value: 'Option B', checked: false },
],
},
},
});
// Add CHECKBOX field
const checkboxField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.CHECKBOX,
page: 1,
positionX: 5,
positionY: 35,
width: 10,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'checkbox',
label: 'Checkbox Field',
values: [
{ id: 1, value: 'Check A', checked: false },
{ id: 2, value: 'Check B', checked: false },
],
},
},
});
// Add DROPDOWN field
const dropdownField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.DROPDOWN,
page: 1,
positionX: 5,
positionY: 45,
width: 10,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'dropdown',
label: 'Dropdown Field',
values: [{ value: 'Select A' }, { value: 'Select B' }],
},
},
});
// 6. Sign in as the user
await apiSignin({
page,
email: user.email,
});
// 7. Navigate to the template
await page.goto(`${WEBAPP_BASE_URL}/templates/${template.id}`);
// 8. Create a document from the template with prefilled fields
const response = await request.post(
`${WEBAPP_BASE_URL}/api/v1/templates/${template.id}/generate-document`,
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
title: 'Document with Prefilled Fields',
recipients: [
{
id: recipient.id,
email: 'recipient@example.com',
name: 'Test Recipient',
role: 'SIGNER',
},
],
prefillFields: [
{
id: textField.id,
type: 'text',
label: 'Prefilled Text',
value: 'This is prefilled text',
},
{
id: numberField.id,
type: 'number',
label: 'Prefilled Number',
value: '42',
},
{
id: radioField.id,
type: 'radio',
label: 'Prefilled Radio',
value: 'Option A',
},
{
id: checkboxField.id,
type: 'checkbox',
label: 'Prefilled Checkbox',
value: ['Check A', 'Check B'],
},
{
id: dropdownField.id,
type: 'dropdown',
label: 'Prefilled Dropdown',
value: 'Select B',
},
],
},
},
);
const responseData = await response.json();
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
expect(responseData.documentId).toBeDefined();
// 9. Verify the document was created with prefilled fields
const document = await prisma.document.findUnique({
where: {
id: responseData.documentId,
},
include: {
fields: true,
},
});
expect(document).not.toBeNull();
// 10. Verify each field has the correct prefilled values
const documentTextField = document?.fields.find(
(field) => field.type === FieldType.TEXT && field.fieldMeta?.type === 'text',
);
expect(documentTextField?.fieldMeta).toMatchObject({
type: 'text',
label: 'Prefilled Text',
text: 'This is prefilled text',
});
const documentNumberField = document?.fields.find(
(field) => field.type === FieldType.NUMBER && field.fieldMeta?.type === 'number',
);
expect(documentNumberField?.fieldMeta).toMatchObject({
type: 'number',
label: 'Prefilled Number',
value: '42',
});
const documentRadioField = document?.fields.find(
(field) => field.type === FieldType.RADIO && field.fieldMeta?.type === 'radio',
);
expect(documentRadioField?.fieldMeta).toMatchObject({
type: 'radio',
label: 'Prefilled Radio',
});
// Check that the correct radio option is selected
const radioValues = (documentRadioField?.fieldMeta as TRadioFieldMeta)?.values || [];
const selectedRadioOption = radioValues.find((option) => option.checked);
expect(selectedRadioOption?.value).toBe('Option A');
const documentCheckboxField = document?.fields.find(
(field) => field.type === FieldType.CHECKBOX && field.fieldMeta?.type === 'checkbox',
);
expect(documentCheckboxField?.fieldMeta).toMatchObject({
type: 'checkbox',
label: 'Prefilled Checkbox',
});
// Check that the correct checkbox options are selected
const checkboxValues = (documentCheckboxField?.fieldMeta as TCheckboxFieldMeta)?.values || [];
const checkedOptions = checkboxValues.filter((option) => option.checked);
expect(checkedOptions.length).toBe(2);
expect(checkedOptions.map((option) => option.value)).toContain('Check A');
expect(checkedOptions.map((option) => option.value)).toContain('Check B');
const documentDropdownField = document?.fields.find(
(field) => field.type === FieldType.DROPDOWN && field.fieldMeta?.type === 'dropdown',
);
expect(documentDropdownField?.fieldMeta).toMatchObject({
type: 'dropdown',
label: 'Prefilled Dropdown',
defaultValue: 'Select B',
});
// 11. Sign in as the recipient and verify the prefilled fields are visible
const documentRecipient = await prisma.recipient.findFirst({
where: {
documentId: document?.id,
email: 'recipient@example.com',
},
});
// Send the document to the recipient
const sendResponse = await request.post(
`${WEBAPP_BASE_URL}/api/v1/documents/${document?.id}/send`,
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
sendEmail: false,
},
},
);
expect(sendResponse.ok()).toBeTruthy();
expect(sendResponse.status()).toBe(200);
expect(documentRecipient).not.toBeNull();
// Visit the signing page
await page.goto(`${WEBAPP_BASE_URL}/sign/${documentRecipient?.token}`);
// Verify the prefilled fields are visible with correct values
// Text field
await expect(page.getByText('This is prefilled')).toBeVisible();
// Number field
await expect(page.getByText('42')).toBeVisible();
// Radio field
await expect(page.getByText('Option A')).toBeVisible();
await expect(page.getByRole('radio', { name: 'Option A' })).toBeChecked();
// Checkbox field
await expect(page.getByText('Check A')).toBeVisible();
await expect(page.getByText('Check B')).toBeVisible();
await expect(page.getByRole('checkbox', { name: 'Check A' })).toBeChecked();
await expect(page.getByRole('checkbox', { name: 'Check B' })).toBeChecked();
// Dropdown field
await expect(page.getByText('Select B')).toBeVisible();
});
test('should create a document from template without prefilled fields', async ({
page,
request,
}) => {
// 1. Create a user
const user = await seedUser();
// 2. Create an API token for the user
const { token } = await createApiToken({
userId: user.id,
tokenName: 'test-token',
expiresIn: null,
});
// 3. Create a template with seedBlankTemplate
const template = await seedBlankTemplate(user, {
createTemplateOptions: {
title: 'Template with Default Fields',
},
});
// 4. Create a recipient for the template
const recipient = await prisma.recipient.create({
data: {
templateId: template.id,
email: 'recipient@example.com',
name: 'Test Recipient',
role: RecipientRole.SIGNER,
token: 'test-token',
readStatus: 'NOT_OPENED',
sendStatus: 'NOT_SENT',
signingStatus: 'NOT_SIGNED',
},
});
// 5. Add fields to the template
// Add TEXT field
const textField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.TEXT,
page: 1,
positionX: 5,
positionY: 5,
width: 10,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'text',
label: 'Default Text Field',
},
},
});
// Add NUMBER field
const numberField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.NUMBER,
page: 1,
positionX: 5,
positionY: 15,
width: 10,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'number',
label: 'Default Number Field',
},
},
});
// 6. Sign in as the user
await apiSignin({
page,
email: user.email,
});
// 7. Navigate to the template
await page.goto(`${WEBAPP_BASE_URL}/templates/${template.id}`);
// 8. Create a document from the template without prefilled fields
const response = await request.post(
`${WEBAPP_BASE_URL}/api/v1/templates/${template.id}/generate-document`,
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
title: 'Document with Default Fields',
recipients: [
{
id: recipient.id,
email: 'recipient@example.com',
name: 'Test Recipient',
role: 'SIGNER',
},
],
},
},
);
const responseData = await response.json();
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
expect(responseData.documentId).toBeDefined();
// 9. Verify the document was created with default fields
const document = await prisma.document.findUnique({
where: {
id: responseData.documentId,
},
include: {
fields: true,
},
});
expect(document).not.toBeNull();
// 10. Verify fields have their default values
const documentTextField = document?.fields.find((field) => field.type === FieldType.TEXT);
expect(documentTextField?.fieldMeta).toMatchObject({
type: 'text',
label: 'Default Text Field',
});
const documentNumberField = document?.fields.find((field) => field.type === FieldType.NUMBER);
expect(documentNumberField?.fieldMeta).toMatchObject({
type: 'number',
label: 'Default Number Field',
});
// 11. Sign in as the recipient and verify the default fields are visible
const documentRecipient = await prisma.recipient.findFirst({
where: {
documentId: document?.id,
email: 'recipient@example.com',
},
});
expect(documentRecipient).not.toBeNull();
const sendResponse = await request.post(
`${WEBAPP_BASE_URL}/api/v1/documents/${document?.id}/send`,
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
sendEmail: false,
},
},
);
expect(sendResponse.ok()).toBeTruthy();
expect(sendResponse.status()).toBe(200);
// Visit the signing page
await page.goto(`${WEBAPP_BASE_URL}/sign/${documentRecipient?.token}`);
// Verify the default fields are visible with correct labels
await expect(page.getByText('Default Text Field')).toBeVisible();
await expect(page.getByText('Default Number Field')).toBeVisible();
});
test('should handle invalid field prefill values', async ({ request }) => {
// 1. Create a user
const user = await seedUser();
// 2. Create an API token for the user
const { token } = await createApiToken({
userId: user.id,
tokenName: 'test-token',
expiresIn: null,
});
// 3. Create a template using seedBlankTemplate
const template = await seedBlankTemplate(user, {
createTemplateOptions: {
title: 'Template for Invalid Test',
visibility: 'EVERYONE',
},
});
// 4. Create a recipient for the template
const recipient = await prisma.recipient.create({
data: {
templateId: template.id,
email: 'recipient@example.com',
name: 'Test Recipient',
role: RecipientRole.SIGNER,
token: 'test-token',
readStatus: 'NOT_OPENED',
sendStatus: 'NOT_SENT',
signingStatus: 'NOT_SIGNED',
},
});
// 5. Add a field to the template
const field = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.RADIO,
page: 1,
positionX: 100,
positionY: 100,
width: 100,
height: 50,
customText: '',
inserted: false,
fieldMeta: {
type: 'radio',
label: 'Radio Field',
values: [
{ id: 1, value: 'Option A', checked: false },
{ id: 2, value: 'Option B', checked: false },
],
},
},
});
// 6. Try to create a document with invalid prefill value
const response = await request.post(
`${WEBAPP_BASE_URL}/api/v1/templates/${template.id}/generate-document`,
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
title: 'Document with Invalid Prefill',
recipients: [
{
id: recipient.id,
email: 'recipient@example.com',
name: 'Test Recipient',
role: 'SIGNER',
},
],
prefillFields: [
{
id: field.id,
type: 'radio',
label: 'Invalid Radio',
value: 'Non-existent Option', // This option doesn't exist
},
],
},
},
);
// 7. Verify the request fails with appropriate error
expect(response.ok()).toBeFalsy();
expect(response.status()).toBe(400);
const errorData = await response.json();
expect(errorData.message).toContain('not found in options for RADIO field');
});
});

View File

@@ -0,0 +1,602 @@
import { expect, test } from '@playwright/test';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import type { TCheckboxFieldMeta, TRadioFieldMeta } from '@documenso/lib/types/field-meta';
import { prisma } from '@documenso/prisma';
import { FieldType, RecipientRole } from '@documenso/prisma/client';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import { apiSignin } from '../../fixtures/authentication';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
test.describe('Template Field Prefill API v2', () => {
test('should create a document from template with prefilled fields', async ({
page,
request,
}) => {
// 1. Create a user
const user = await seedUser();
// 2. Create an API token for the user
const { token } = await createApiToken({
userId: user.id,
tokenName: 'test-token',
expiresIn: null,
});
// 3. Create a template with seedBlankTemplate
const template = await seedBlankTemplate(user, {
createTemplateOptions: {
title: 'Template with Advanced Fields V2',
},
});
// 4. Create a recipient for the template
const recipient = await prisma.recipient.create({
data: {
templateId: template.id,
email: 'recipient@example.com',
name: 'Test Recipient',
role: RecipientRole.SIGNER,
token: 'test-token',
readStatus: 'NOT_OPENED',
sendStatus: 'NOT_SENT',
signingStatus: 'NOT_SIGNED',
},
});
// 5. Add fields to the template
// Add TEXT field
const textField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.TEXT,
page: 1,
positionX: 5,
positionY: 5,
width: 20,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'text',
label: 'Text Field',
},
},
});
// Add NUMBER field
const numberField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.NUMBER,
page: 1,
positionX: 5,
positionY: 15,
width: 20,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'number',
label: 'Number Field',
},
},
});
// Add RADIO field
const radioField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.RADIO,
page: 1,
positionX: 5,
positionY: 25,
width: 20,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'radio',
label: 'Radio Field',
values: [
{ id: 1, value: 'Option A', checked: false },
{ id: 2, value: 'Option B', checked: false },
],
},
},
});
// Add CHECKBOX field
const checkboxField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.CHECKBOX,
page: 1,
positionX: 5,
positionY: 35,
width: 20,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'checkbox',
label: 'Checkbox Field',
values: [
{ id: 1, value: 'Check A', checked: false },
{ id: 2, value: 'Check B', checked: false },
],
},
},
});
// Add DROPDOWN field
const dropdownField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.DROPDOWN,
page: 1,
positionX: 5,
positionY: 45,
width: 20,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'dropdown',
label: 'Dropdown Field',
values: [{ value: 'Select A' }, { value: 'Select B' }],
},
},
});
// 6. Sign in as the user
await apiSignin({
page,
email: user.email,
});
// 7. Navigate to the template
await page.goto(`${WEBAPP_BASE_URL}/templates/${template.id}`);
// 8. Create a document from the template with prefilled fields using v2 API
const response = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/use`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
templateId: template.id,
recipients: [
{
id: recipient.id,
email: 'recipient@example.com',
name: 'Test Recipient',
},
],
prefillFields: [
{
id: textField.id,
type: 'text',
label: 'Prefilled Text',
value: 'This is prefilled text',
},
{
id: numberField.id,
type: 'number',
label: 'Prefilled Number',
value: '42',
},
{
id: radioField.id,
type: 'radio',
label: 'Prefilled Radio',
value: 'Option A',
},
{
id: checkboxField.id,
type: 'checkbox',
label: 'Prefilled Checkbox',
value: ['Check A', 'Check B'],
},
{
id: dropdownField.id,
type: 'dropdown',
label: 'Prefilled Dropdown',
value: 'Select B',
},
],
},
});
const responseData = await response.json();
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
expect(responseData.id).toBeDefined();
// 9. Verify the document was created with prefilled fields
const document = await prisma.document.findUnique({
where: {
id: responseData.id,
},
include: {
fields: true,
},
});
expect(document).not.toBeNull();
// 10. Verify each field has the correct prefilled values
const documentTextField = document?.fields.find(
(field) => field.type === FieldType.TEXT && field.fieldMeta?.type === 'text',
);
expect(documentTextField?.fieldMeta).toMatchObject({
type: 'text',
label: 'Prefilled Text',
text: 'This is prefilled text',
});
const documentNumberField = document?.fields.find(
(field) => field.type === FieldType.NUMBER && field.fieldMeta?.type === 'number',
);
expect(documentNumberField?.fieldMeta).toMatchObject({
type: 'number',
label: 'Prefilled Number',
value: '42',
});
const documentRadioField = document?.fields.find(
(field) => field.type === FieldType.RADIO && field.fieldMeta?.type === 'radio',
);
expect(documentRadioField?.fieldMeta).toMatchObject({
type: 'radio',
label: 'Prefilled Radio',
});
// Check that the correct radio option is selected
const radioValues = (documentRadioField?.fieldMeta as TRadioFieldMeta)?.values || [];
const selectedRadioOption = radioValues.find((option) => option.checked);
expect(selectedRadioOption?.value).toBe('Option A');
const documentCheckboxField = document?.fields.find(
(field) => field.type === FieldType.CHECKBOX && field.fieldMeta?.type === 'checkbox',
);
expect(documentCheckboxField?.fieldMeta).toMatchObject({
type: 'checkbox',
label: 'Prefilled Checkbox',
});
// Check that the correct checkbox options are selected
const checkboxValues = (documentCheckboxField?.fieldMeta as TCheckboxFieldMeta)?.values || [];
const checkedOptions = checkboxValues.filter((option) => option.checked);
expect(checkedOptions.length).toBe(2);
expect(checkedOptions.map((option) => option.value)).toContain('Check A');
expect(checkedOptions.map((option) => option.value)).toContain('Check B');
const documentDropdownField = document?.fields.find(
(field) => field.type === FieldType.DROPDOWN && field.fieldMeta?.type === 'dropdown',
);
expect(documentDropdownField?.fieldMeta).toMatchObject({
type: 'dropdown',
label: 'Prefilled Dropdown',
defaultValue: 'Select B',
});
const sendResponse = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/distribute`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
documentId: document?.id,
meta: {
subject: 'Test Subject',
message: 'Test Message',
},
},
});
await expect(sendResponse.ok()).toBeTruthy();
await expect(sendResponse.status()).toBe(200);
// 11. Sign in as the recipient and verify the prefilled fields are visible
const documentRecipient = await prisma.recipient.findFirst({
where: {
documentId: document?.id,
email: 'recipient@example.com',
},
});
expect(documentRecipient).not.toBeNull();
// Visit the signing page
await page.goto(`${WEBAPP_BASE_URL}/sign/${documentRecipient?.token}`);
// Verify the prefilled fields are visible with correct values
// Text field
await expect(page.getByText('This is prefilled')).toBeVisible();
// Number field
await expect(page.getByText('42')).toBeVisible();
// Radio field
await expect(page.getByText('Option A')).toBeVisible();
await expect(page.getByRole('radio', { name: 'Option A' })).toBeChecked();
// Checkbox field
await expect(page.getByText('Check A')).toBeVisible();
await expect(page.getByText('Check B')).toBeVisible();
await expect(page.getByRole('checkbox', { name: 'Check A' })).toBeChecked();
await expect(page.getByRole('checkbox', { name: 'Check B' })).toBeChecked();
// Dropdown field
await expect(page.getByText('Select B')).toBeVisible();
});
test('should create a document from template without prefilled fields', async ({
page,
request,
}) => {
// 1. Create a user
const user = await seedUser();
// 2. Create an API token for the user
const { token } = await createApiToken({
userId: user.id,
tokenName: 'test-token',
expiresIn: null,
});
// 3. Create a template with seedBlankTemplate
const template = await seedBlankTemplate(user, {
createTemplateOptions: {
title: 'Template with Default Fields V2',
},
});
// 4. Create a recipient for the template
const recipient = await prisma.recipient.create({
data: {
templateId: template.id,
email: 'recipient@example.com',
name: 'Test Recipient',
role: RecipientRole.SIGNER,
token: 'test-token',
readStatus: 'NOT_OPENED',
sendStatus: 'NOT_SENT',
signingStatus: 'NOT_SIGNED',
},
});
// 5. Add fields to the template
// Add TEXT field
const textField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.TEXT,
page: 1,
positionX: 5,
positionY: 5,
width: 20,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'text',
label: 'Default Text Field',
},
},
});
// Add NUMBER field
const numberField = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.NUMBER,
page: 1,
positionX: 5,
positionY: 15,
width: 20,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'number',
label: 'Default Number Field',
},
},
});
// 6. Sign in as the user
await apiSignin({
page,
email: user.email,
});
// 7. Navigate to the template
await page.goto(`${WEBAPP_BASE_URL}/templates/${template.id}`);
// 8. Create a document from the template without prefilled fields using v2 API
const response = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/use`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
templateId: template.id,
recipients: [
{
id: recipient.id,
email: 'recipient@example.com',
name: 'Test Recipient',
},
],
},
});
const responseData = await response.json();
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
expect(responseData.id).toBeDefined();
// 9. Verify the document was created with default fields
const document = await prisma.document.findUnique({
where: {
id: responseData.id,
},
include: {
fields: true,
},
});
expect(document).not.toBeNull();
// 10. Verify fields have their default values
const documentTextField = document?.fields.find((field) => field.type === FieldType.TEXT);
expect(documentTextField?.fieldMeta).toMatchObject({
type: 'text',
label: 'Default Text Field',
});
const documentNumberField = document?.fields.find((field) => field.type === FieldType.NUMBER);
expect(documentNumberField?.fieldMeta).toMatchObject({
type: 'number',
label: 'Default Number Field',
});
const sendResponse = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/distribute`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
documentId: document?.id,
meta: {
subject: 'Test Subject',
message: 'Test Message',
},
},
});
await expect(sendResponse.ok()).toBeTruthy();
await expect(sendResponse.status()).toBe(200);
// 11. Sign in as the recipient and verify the default fields are visible
const documentRecipient = await prisma.recipient.findFirst({
where: {
documentId: document?.id,
email: 'recipient@example.com',
},
});
expect(documentRecipient).not.toBeNull();
// Visit the signing page
await page.goto(`${WEBAPP_BASE_URL}/sign/${documentRecipient?.token}`);
await expect(page.getByText('This is prefilled')).not.toBeVisible();
});
test('should handle invalid field prefill values', async ({ request }) => {
// 1. Create a user
const user = await seedUser();
// 2. Create an API token for the user
const { token } = await createApiToken({
userId: user.id,
tokenName: 'test-token',
expiresIn: null,
});
// 3. Create a template using seedBlankTemplate
const template = await seedBlankTemplate(user, {
createTemplateOptions: {
title: 'Template for Invalid Test V2',
visibility: 'EVERYONE',
},
});
// 4. Create a recipient for the template
const recipient = await prisma.recipient.create({
data: {
templateId: template.id,
email: 'recipient@example.com',
name: 'Test Recipient',
role: RecipientRole.SIGNER,
token: 'test-token',
readStatus: 'NOT_OPENED',
sendStatus: 'NOT_SENT',
signingStatus: 'NOT_SIGNED',
},
});
// 5. Add a field to the template
const field = await prisma.field.create({
data: {
templateId: template.id,
recipientId: recipient.id,
type: FieldType.RADIO,
page: 1,
positionX: 100,
positionY: 100,
width: 100,
height: 50,
customText: '',
inserted: false,
fieldMeta: {
type: 'radio',
label: 'Radio Field',
values: [
{ id: 1, value: 'Option A', checked: false },
{ id: 2, value: 'Option B', checked: false },
],
},
},
});
// 7. Try to create a document with invalid prefill value
const response = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/use`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
templateId: template.id,
recipients: [
{
id: recipient.id,
email: 'recipient@example.com',
name: 'Test Recipient',
},
],
prefillFields: [
{
id: field.id,
type: 'radio',
label: 'Invalid Radio',
value: 'Non-existent Option', // This option doesn't exist
},
],
},
});
// 8. Verify the request fails with appropriate error
expect(response.ok()).toBeFalsy();
expect(response.status()).toBe(400);
const errorData = await response.json();
expect(errorData.message).toContain('not found in options for RADIO field');
});
});

View File

@@ -377,7 +377,9 @@ test('[DOCUMENT_FLOW]: should be able to approve a document', async ({ page }) =
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true'); await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
} }
await page.getByRole('button', { name: 'Complete' }).click(); await page
.getByRole('button', { name: role === RecipientRole.SIGNER ? 'Complete' : 'Approve' })
.click();
await page await page
.getByRole('button', { name: role === RecipientRole.SIGNER ? 'Sign' : 'Approve' }) .getByRole('button', { name: role === RecipientRole.SIGNER ? 'Sign' : 'Approve' })
.click(); .click();
@@ -447,7 +449,7 @@ test('[DOCUMENT_FLOW]: should be able to create, send with redirect url, sign a
const { status } = await getDocumentByToken(token); const { status } = await getDocumentByToken(token);
expect(status).toBe(DocumentStatus.PENDING); expect(status).toBe(DocumentStatus.PENDING);
await page.getByRole('button', { name: 'Complete' }).click(); await page.getByRole('button', { name: 'Approve' }).click();
await expect(page.getByRole('dialog').getByText('Complete Approval').first()).toBeVisible(); await expect(page.getByRole('dialog').getByText('Complete Approval').first()).toBeVisible();
await page.getByRole('button', { name: 'Approve' }).click(); await page.getByRole('button', { name: 'Approve' }).click();

View File

@@ -8,6 +8,7 @@ import { SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION } from './definitions/emails/sen
import { SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-team-member-joined-email'; import { SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-team-member-joined-email';
import { SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-team-member-left-email'; import { SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-team-member-left-email';
import { BULK_SEND_TEMPLATE_JOB_DEFINITION } from './definitions/internal/bulk-send-template'; import { BULK_SEND_TEMPLATE_JOB_DEFINITION } from './definitions/internal/bulk-send-template';
import { EXECUTE_WEBHOOK_JOB_DEFINITION } from './definitions/internal/execute-webhook';
import { SEAL_DOCUMENT_JOB_DEFINITION } from './definitions/internal/seal-document'; import { SEAL_DOCUMENT_JOB_DEFINITION } from './definitions/internal/seal-document';
/** /**
@@ -25,6 +26,7 @@ export const jobsClient = new JobClient([
SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION, SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION,
SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION, SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION,
BULK_SEND_TEMPLATE_JOB_DEFINITION, BULK_SEND_TEMPLATE_JOB_DEFINITION,
EXECUTE_WEBHOOK_JOB_DEFINITION,
] as const); ] as const);
export const jobs = jobsClient; export const jobs = jobsClient;

View File

@@ -0,0 +1,74 @@
import { Prisma, WebhookCallStatus } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import type { JobRunIO } from '../../client/_internal/job';
import type { TExecuteWebhookJobDefinition } from './execute-webhook';
export const run = async ({
payload,
io,
}: {
payload: TExecuteWebhookJobDefinition;
io: JobRunIO;
}) => {
const { event, webhookId, data } = payload;
const webhook = await prisma.webhook.findUniqueOrThrow({
where: {
id: webhookId,
},
});
const { webhookUrl: url, secret } = webhook;
await io.runTask('execute-webhook', async () => {
const payloadData = {
event,
payload: data,
createdAt: new Date().toISOString(),
webhookEndpoint: url,
};
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(payloadData),
headers: {
'Content-Type': 'application/json',
'X-Documenso-Secret': secret ?? '',
},
});
const body = await response.text();
let responseBody: Prisma.InputJsonValue | Prisma.JsonNullValueInput = Prisma.JsonNull;
try {
responseBody = JSON.parse(body);
} catch (err) {
responseBody = body;
}
await prisma.webhookCall.create({
data: {
url,
event,
status: response.ok ? WebhookCallStatus.SUCCESS : WebhookCallStatus.FAILED,
requestBody: payloadData as Prisma.InputJsonValue,
responseCode: response.status,
responseBody,
responseHeaders: Object.fromEntries(response.headers.entries()),
webhookId: webhook.id,
},
});
if (!response.ok) {
throw new Error(`Webhook execution failed with status ${response.status}`);
}
return {
success: response.ok,
status: response.status,
};
});
};

View File

@@ -0,0 +1,34 @@
import { WebhookTriggerEvents } from '@prisma/client';
import { z } from 'zod';
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
import { type JobDefinition } from '../../client/_internal/job';
const EXECUTE_WEBHOOK_JOB_DEFINITION_ID = 'internal.execute-webhook';
const EXECUTE_WEBHOOK_JOB_DEFINITION_SCHEMA = z.object({
event: z.nativeEnum(WebhookTriggerEvents),
webhookId: z.string(),
data: z.any(), // fix
requestMetadata: ZRequestMetadataSchema.optional(),
});
export type TExecuteWebhookJobDefinition = z.infer<typeof EXECUTE_WEBHOOK_JOB_DEFINITION_SCHEMA>;
export const EXECUTE_WEBHOOK_JOB_DEFINITION = {
id: EXECUTE_WEBHOOK_JOB_DEFINITION_ID,
name: 'Execute Webhook',
version: '1.0.0',
trigger: {
name: EXECUTE_WEBHOOK_JOB_DEFINITION_ID,
schema: EXECUTE_WEBHOOK_JOB_DEFINITION_SCHEMA,
},
handler: async ({ payload, io }) => {
const handler = await import('./execute-webhook.handler');
await handler.run({ payload, io });
},
} as const satisfies JobDefinition<
typeof EXECUTE_WEBHOOK_JOB_DEFINITION_ID,
TExecuteWebhookJobDefinition
>;

View File

@@ -37,6 +37,7 @@ import {
mapDocumentToWebhookDocumentPayload, mapDocumentToWebhookDocumentPayload,
} from '../../types/webhook-payload'; } from '../../types/webhook-payload';
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata'; import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
import { isRequiredField } from '../../utils/advanced-fields-helpers';
import type { CreateDocumentAuditLogDataResponse } from '../../utils/document-audit-logs'; import type { CreateDocumentAuditLogDataResponse } from '../../utils/document-audit-logs';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { import {
@@ -176,20 +177,28 @@ export const createDocumentFromDirectTemplate = async ({
const metaSigningOrder = template.templateMeta?.signingOrder || DocumentSigningOrder.PARALLEL; const metaSigningOrder = template.templateMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
// Associate, validate and map to a query every direct template recipient field with the provided fields. // Associate, validate and map to a query every direct template recipient field with the provided fields.
// Only process fields that are either required or have been signed by the user
const fieldsToProcess = directTemplateRecipient.fields.filter((templateField) => {
const signedFieldValue = signedFieldValues.find((value) => value.fieldId === templateField.id);
// Include if it's required or has a signed value
return isRequiredField(templateField) || signedFieldValue !== undefined;
});
const createDirectRecipientFieldArgs = await Promise.all( const createDirectRecipientFieldArgs = await Promise.all(
directTemplateRecipient.fields.map(async (templateField) => { fieldsToProcess.map(async (templateField) => {
const signedFieldValue = signedFieldValues.find( const signedFieldValue = signedFieldValues.find(
(value) => value.fieldId === templateField.id, (value) => value.fieldId === templateField.id,
); );
if (!signedFieldValue) { if (isRequiredField(templateField) && !signedFieldValue) {
throw new AppError(AppErrorCode.INVALID_BODY, { throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid, missing or changed fields', message: 'Invalid, missing or changed fields',
}); });
} }
if (templateField.type === FieldType.NAME && directRecipientName === undefined) { if (templateField.type === FieldType.NAME && directRecipientName === undefined) {
directRecipientName === signedFieldValue.value; directRecipientName === signedFieldValue?.value;
} }
const derivedRecipientActionAuth = await validateFieldAuth({ const derivedRecipientActionAuth = await validateFieldAuth({
@@ -200,9 +209,18 @@ export const createDocumentFromDirectTemplate = async ({
}, },
field: templateField, field: templateField,
userId: user?.id, userId: user?.id,
authOptions: signedFieldValue.authOptions, authOptions: signedFieldValue?.authOptions,
}); });
if (!signedFieldValue) {
return {
templateField,
customText: '',
derivedRecipientActionAuth,
signature: null,
};
}
const { value, isBase64 } = signedFieldValue; const { value, isBase64 } = signedFieldValue;
const isSignatureField = const isSignatureField =
@@ -380,7 +398,7 @@ export const createDocumentFromDirectTemplate = async ({
positionY: templateField.positionY, positionY: templateField.positionY,
width: templateField.width, width: templateField.width,
height: templateField.height, height: templateField.height,
customText, customText: customText ?? '',
inserted: true, inserted: true,
fieldMeta: templateField.fieldMeta || Prisma.JsonNull, fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
})), })),

View File

@@ -9,6 +9,7 @@ import {
SigningStatus, SigningStatus,
WebhookTriggerEvents, WebhookTriggerEvents,
} from '@prisma/client'; } from '@prisma/client';
import { match } from 'ts-pattern';
import { nanoid } from '@documenso/lib/universal/id'; import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
@@ -18,7 +19,20 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs'; import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import { ZRecipientAuthOptionsSchema } from '../../types/document-auth'; import { ZRecipientAuthOptionsSchema } from '../../types/document-auth';
import type { TDocumentEmailSettings } from '../../types/document-email'; import type { TDocumentEmailSettings } from '../../types/document-email';
import { ZFieldMetaSchema } from '../../types/field-meta'; import type {
TCheckboxFieldMeta,
TDropdownFieldMeta,
TFieldMetaPrefillFieldsSchema,
TNumberFieldMeta,
TRadioFieldMeta,
TTextFieldMeta,
} from '../../types/field-meta';
import {
ZCheckboxFieldMeta,
ZDropdownFieldMeta,
ZFieldMetaSchema,
ZRadioFieldMeta,
} from '../../types/field-meta';
import { import {
ZWebhookDocumentSchema, ZWebhookDocumentSchema,
mapDocumentToWebhookDocumentPayload, mapDocumentToWebhookDocumentPayload,
@@ -51,6 +65,7 @@ export type CreateDocumentFromTemplateOptions = {
email: string; email: string;
signingOrder?: number | null; signingOrder?: number | null;
}[]; }[];
prefillFields?: TFieldMetaPrefillFieldsSchema[];
customDocumentDataId?: string; customDocumentDataId?: string;
/** /**
@@ -73,6 +88,175 @@ export type CreateDocumentFromTemplateOptions = {
requestMetadata: ApiRequestMetadata; requestMetadata: ApiRequestMetadata;
}; };
const getUpdatedFieldMeta = (field: Field, prefillField?: TFieldMetaPrefillFieldsSchema) => {
if (!prefillField) {
return field.fieldMeta;
}
const advancedField = ['NUMBER', 'RADIO', 'CHECKBOX', 'DROPDOWN', 'TEXT'].includes(field.type);
if (!advancedField) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Field ${field.id} is not an advanced field and cannot have field meta information. Allowed types: NUMBER, RADIO, CHECKBOX, DROPDOWN, TEXT.`,
});
}
// We've already validated that the field types match at a higher level
// Start with the existing field meta or an empty object
const existingMeta = field.fieldMeta || {};
// Apply type-specific updates based on the prefill field type using ts-pattern
return match(prefillField)
.with({ type: 'text' }, (field) => {
if (typeof field.value !== 'string') {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Invalid value for TEXT field ${field.id}: expected string, got ${typeof field.value}`,
});
}
const meta: TTextFieldMeta = {
...existingMeta,
type: 'text',
label: field.label,
placeholder: field.placeholder,
text: field.value,
};
return meta;
})
.with({ type: 'number' }, (field) => {
if (typeof field.value !== 'string') {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Invalid value for NUMBER field ${field.id}: expected string, got ${typeof field.value}`,
});
}
const meta: TNumberFieldMeta = {
...existingMeta,
type: 'number',
label: field.label,
placeholder: field.placeholder,
value: field.value,
};
return meta;
})
.with({ type: 'radio' }, (field) => {
if (typeof field.value !== 'string') {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Invalid value for RADIO field ${field.id}: expected string, got ${typeof field.value}`,
});
}
const result = ZRadioFieldMeta.safeParse(existingMeta);
if (!result.success) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Invalid field meta for RADIO field ${field.id}`,
});
}
const radioMeta = result.data;
// Validate that the value exists in the options
const valueExists = radioMeta.values?.some((option) => option.value === field.value);
if (!valueExists) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Value "${field.value}" not found in options for RADIO field ${field.id}`,
});
}
const newValues = radioMeta.values?.map((option) => ({
...option,
checked: option.value === field.value,
}));
const meta: TRadioFieldMeta = {
...existingMeta,
type: 'radio',
label: field.label,
values: newValues,
};
return meta;
})
.with({ type: 'checkbox' }, (field) => {
const result = ZCheckboxFieldMeta.safeParse(existingMeta);
if (!result.success) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Invalid field meta for CHECKBOX field ${field.id}`,
});
}
const checkboxMeta = result.data;
if (!field.value) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Value is required for CHECKBOX field ${field.id}`,
});
}
const fieldValue = field.value;
// Validate that all values exist in the options
for (const value of fieldValue) {
const valueExists = checkboxMeta.values?.some((option) => option.value === value);
if (!valueExists) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Value "${value}" not found in options for CHECKBOX field ${field.id}`,
});
}
}
const newValues = checkboxMeta.values?.map((option) => ({
...option,
checked: fieldValue.includes(option.value),
}));
const meta: TCheckboxFieldMeta = {
...existingMeta,
type: 'checkbox',
label: field.label,
values: newValues,
};
return meta;
})
.with({ type: 'dropdown' }, (field) => {
const result = ZDropdownFieldMeta.safeParse(existingMeta);
if (!result.success) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Invalid field meta for DROPDOWN field ${field.id}`,
});
}
const dropdownMeta = result.data;
// Validate that the value exists in the options if values are defined
const valueExists = dropdownMeta.values?.some((option) => option.value === field.value);
if (!valueExists) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Value "${field.value}" not found in options for DROPDOWN field ${field.id}`,
});
}
const meta: TDropdownFieldMeta = {
...existingMeta,
type: 'dropdown',
label: field.label,
defaultValue: field.value,
};
return meta;
})
.otherwise(() => field.fieldMeta);
};
export const createDocumentFromTemplate = async ({ export const createDocumentFromTemplate = async ({
templateId, templateId,
externalId, externalId,
@@ -82,6 +266,7 @@ export const createDocumentFromTemplate = async ({
customDocumentDataId, customDocumentDataId,
override, override,
requestMetadata, requestMetadata,
prefillFields,
}: CreateDocumentFromTemplateOptions) => { }: CreateDocumentFromTemplateOptions) => {
const template = await prisma.template.findUnique({ const template = await prisma.template.findUnique({
where: { where: {
@@ -259,6 +444,47 @@ export const createDocumentFromTemplate = async ({
let fieldsToCreate: Omit<Field, 'id' | 'secondaryId' | 'templateId'>[] = []; let fieldsToCreate: Omit<Field, 'id' | 'secondaryId' | 'templateId'>[] = [];
// Get all template field IDs first so we can validate later
const allTemplateFieldIds = finalRecipients.flatMap((recipient) =>
recipient.fields.map((field) => field.id),
);
if (prefillFields?.length) {
// Validate that all prefill field IDs exist in the template
const invalidFieldIds = prefillFields
.map((prefillField) => prefillField.id)
.filter((id) => !allTemplateFieldIds.includes(id));
if (invalidFieldIds.length > 0) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `The following field IDs do not exist in the template: ${invalidFieldIds.join(', ')}`,
});
}
// Validate that all prefill fields have the correct type
for (const prefillField of prefillFields) {
const templateField = finalRecipients
.flatMap((recipient) => recipient.fields)
.find((field) => field.id === prefillField.id);
if (!templateField) {
// This should never happen due to the previous validation, but just in case
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Field with ID ${prefillField.id} not found in the template`,
});
}
const expectedType = templateField.type.toLowerCase();
const actualType = prefillField.type;
if (expectedType !== actualType) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Field type mismatch for field ${prefillField.id}: expected ${expectedType}, got ${actualType}`,
});
}
}
}
Object.values(finalRecipients).forEach(({ email, fields }) => { Object.values(finalRecipients).forEach(({ email, fields }) => {
const recipient = document.recipients.find((recipient) => recipient.email === email); const recipient = document.recipients.find((recipient) => recipient.email === email);
@@ -267,7 +493,12 @@ export const createDocumentFromTemplate = async ({
} }
fieldsToCreate = fieldsToCreate.concat( fieldsToCreate = fieldsToCreate.concat(
fields.map((field) => ({ fields.map((field) => {
const prefillField = prefillFields?.find((value) => value.id === field.id);
// Use type assertion to help TypeScript understand the structure
const updatedFieldMeta = getUpdatedFieldMeta(field, prefillField);
return {
documentId: document.id, documentId: document.id,
recipientId: recipient.id, recipientId: recipient.id,
type: field.type, type: field.type,
@@ -278,8 +509,9 @@ export const createDocumentFromTemplate = async ({
height: field.height, height: field.height,
customText: '', customText: '',
inserted: false, inserted: false,
fieldMeta: field.fieldMeta, fieldMeta: updatedFieldMeta,
})), };
}),
); );
}); });

View File

@@ -1,54 +0,0 @@
import { Prisma, type Webhook, WebhookCallStatus, type WebhookTriggerEvents } from '@prisma/client';
import { prisma } from '@documenso/prisma';
export type ExecuteWebhookOptions = {
event: WebhookTriggerEvents;
webhook: Webhook;
data: unknown;
};
export const executeWebhook = async ({ event, webhook, data }: ExecuteWebhookOptions) => {
const { webhookUrl: url, secret } = webhook;
console.log('Executing webhook', { event, url });
const payload = {
event,
payload: data,
createdAt: new Date().toISOString(),
webhookEndpoint: url,
};
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json',
'X-Documenso-Secret': secret ?? '',
},
});
const body = await response.text();
let responseBody: Prisma.InputJsonValue | Prisma.JsonNullValueInput = Prisma.JsonNull;
try {
responseBody = JSON.parse(body);
} catch (err) {
responseBody = body;
}
await prisma.webhookCall.create({
data: {
url,
event,
status: response.ok ? WebhookCallStatus.SUCCESS : WebhookCallStatus.FAILED,
requestBody: payload as Prisma.InputJsonValue,
responseCode: response.status,
responseBody,
responseHeaders: Object.fromEntries(response.headers.entries()),
webhookId: webhook.id,
},
});
};

View File

@@ -1,6 +1,6 @@
import { jobs } from '../../../jobs/client';
import { verify } from '../../crypto/verify'; import { verify } from '../../crypto/verify';
import { getAllWebhooksByEventTrigger } from '../get-all-webhooks-by-event-trigger'; import { getAllWebhooksByEventTrigger } from '../get-all-webhooks-by-event-trigger';
import { executeWebhook } from './execute-webhook';
import { ZTriggerWebhookBodySchema } from './schema'; import { ZTriggerWebhookBodySchema } from './schema';
export type HandlerTriggerWebhooksResponse = export type HandlerTriggerWebhooksResponse =
@@ -21,14 +21,16 @@ export const handlerTriggerWebhooks = async (req: Request) => {
return Response.json({ success: false, error: 'Missing signature' }, { status: 400 }); return Response.json({ success: false, error: 'Missing signature' }, { status: 400 });
} }
const valid = verify(req.body, signature); const body = await req.json();
const valid = verify(body, signature);
if (!valid) { if (!valid) {
console.log('Invalid signature'); console.log('Invalid signature');
return Response.json({ success: false, error: 'Invalid signature' }, { status: 400 }); return Response.json({ success: false, error: 'Invalid signature' }, { status: 400 });
} }
const result = ZTriggerWebhookBodySchema.safeParse(req.body); const result = ZTriggerWebhookBodySchema.safeParse(body);
if (!result.success) { if (!result.success) {
console.log('Invalid request body'); console.log('Invalid request body');
@@ -40,17 +42,20 @@ export const handlerTriggerWebhooks = async (req: Request) => {
const allWebhooks = await getAllWebhooksByEventTrigger({ event, userId, teamId }); const allWebhooks = await getAllWebhooksByEventTrigger({ event, userId, teamId });
await Promise.allSettled( await Promise.allSettled(
allWebhooks.map(async (webhook) => allWebhooks.map(async (webhook) => {
executeWebhook({ await jobs.triggerJob({
name: 'internal.execute-webhook',
payload: {
event, event,
webhook, webhookId: webhook.id,
data, data,
},
});
}), }),
),
); );
return Response.json( return Response.json(
{ success: true, message: 'Webhooks executed successfully' }, { success: true, message: 'Webhooks queued for execution' },
{ status: 200 }, { status: 200 },
); );
}; };

View File

@@ -473,6 +473,7 @@ msgstr "<0>Sie sind dabei, die Genehmigung von <1>\"{documentTitle}\"</1> abzusc
msgid "<0>You are about to complete signing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?" msgid "<0>You are about to complete signing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?"
msgstr "<0>Sie sind dabei, die Unterzeichnung von \"<1>{documentTitle}</1>\" abzuschließen.</0><2/> Sind Sie sicher?" msgstr "<0>Sie sind dabei, die Unterzeichnung von \"<1>{documentTitle}</1>\" abzuschließen.</0><2/> Sind Sie sicher?"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "<0>You are about to complete viewing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?" msgid "<0>You are about to complete viewing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?"
msgstr "<0>Sie sind dabei, die Ansicht von \"<1>{documentTitle}</1>\" abzuschließen.</0><2/> Sind Sie sicher?" msgstr "<0>Sie sind dabei, die Ansicht von \"<1>{documentTitle}</1>\" abzuschließen.</0><2/> Sind Sie sicher?"
@@ -1144,6 +1145,7 @@ msgstr "App-Version"
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/tables/documents-table-action-button.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-button.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx
#: packages/lib/constants/recipient-roles.ts #: packages/lib/constants/recipient-roles.ts
msgid "Approve" msgid "Approve"
@@ -1565,6 +1567,7 @@ msgstr "Klicken, um das Feld auszufüllen"
msgid "Close" msgid "Close"
msgstr "Schließen" msgstr "Schließen"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/embed/embed-document-signing-page.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
@@ -1576,6 +1579,10 @@ msgstr "Abschließen"
msgid "Complete Approval" msgid "Complete Approval"
msgstr "Genehmigung abschließen" msgstr "Genehmigung abschließen"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Complete Assisting"
msgstr ""
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx #: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Complete Document" msgid "Complete Document"
msgstr "" msgstr ""
@@ -1588,6 +1595,7 @@ msgstr "Unterzeichnung abschließen"
msgid "Complete the fields for the following signers. Once reviewed, they will inform you if any modifications are needed." msgid "Complete the fields for the following signers. Once reviewed, they will inform you if any modifications are needed."
msgstr "" msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Complete Viewing" msgid "Complete Viewing"
msgstr "Betrachten abschließen" msgstr "Betrachten abschließen"
@@ -3373,6 +3381,11 @@ msgstr "Verwalten Sie hier Ihre Seiteneinstellungen"
msgid "Manager" msgid "Manager"
msgstr "Manager" msgstr "Manager"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Mark as viewed"
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Mark as Viewed" msgid "Mark as Viewed"
msgstr "Als angesehen markieren" msgstr "Als angesehen markieren"
@@ -3988,6 +4001,10 @@ msgstr "Bitte geben Sie ein Token von der Authentifizierungs-App oder einen Back
msgid "Please provide a token from your authenticator, or a backup code." msgid "Please provide a token from your authenticator, or a backup code."
msgstr "Bitte geben Sie ein Token von Ihrem Authentifizierer oder einen Backup-Code an." msgstr "Bitte geben Sie ein Token von Ihrem Authentifizierer oder einen Backup-Code an."
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
msgid "Please review the document before approving."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx #: apps/remix/app/components/general/document-signing/document-signing-form.tsx
msgid "Please review the document before signing." msgid "Please review the document before signing."
msgstr "Bitte überprüfen Sie das Dokument vor der Unterzeichnung." msgstr "Bitte überprüfen Sie das Dokument vor der Unterzeichnung."

View File

@@ -468,6 +468,7 @@ msgstr "<0>You are about to complete approving <1>\"{documentTitle}\"</1>.</0><2
msgid "<0>You are about to complete signing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?" msgid "<0>You are about to complete signing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?"
msgstr "<0>You are about to complete signing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?" msgstr "<0>You are about to complete signing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "<0>You are about to complete viewing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?" msgid "<0>You are about to complete viewing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?"
msgstr "<0>You are about to complete viewing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?" msgstr "<0>You are about to complete viewing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?"
@@ -1139,6 +1140,7 @@ msgstr "App Version"
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/tables/documents-table-action-button.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-button.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx
#: packages/lib/constants/recipient-roles.ts #: packages/lib/constants/recipient-roles.ts
msgid "Approve" msgid "Approve"
@@ -1560,6 +1562,7 @@ msgstr "Click to insert field"
msgid "Close" msgid "Close"
msgstr "Close" msgstr "Close"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/embed/embed-document-signing-page.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
@@ -1571,6 +1574,10 @@ msgstr "Complete"
msgid "Complete Approval" msgid "Complete Approval"
msgstr "Complete Approval" msgstr "Complete Approval"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Complete Assisting"
msgstr "Complete Assisting"
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx #: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Complete Document" msgid "Complete Document"
msgstr "Complete Document" msgstr "Complete Document"
@@ -1583,6 +1590,7 @@ msgstr "Complete Signing"
msgid "Complete the fields for the following signers. Once reviewed, they will inform you if any modifications are needed." msgid "Complete the fields for the following signers. Once reviewed, they will inform you if any modifications are needed."
msgstr "Complete the fields for the following signers. Once reviewed, they will inform you if any modifications are needed." msgstr "Complete the fields for the following signers. Once reviewed, they will inform you if any modifications are needed."
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Complete Viewing" msgid "Complete Viewing"
msgstr "Complete Viewing" msgstr "Complete Viewing"
@@ -3368,6 +3376,11 @@ msgstr "Manage your site settings here"
msgid "Manager" msgid "Manager"
msgstr "Manager" msgstr "Manager"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Mark as viewed"
msgstr "Mark as viewed"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Mark as Viewed" msgid "Mark as Viewed"
msgstr "Mark as Viewed" msgstr "Mark as Viewed"
@@ -3983,6 +3996,10 @@ msgstr "Please provide a token from the authenticator, or a backup code. If you
msgid "Please provide a token from your authenticator, or a backup code." msgid "Please provide a token from your authenticator, or a backup code."
msgstr "Please provide a token from your authenticator, or a backup code." msgstr "Please provide a token from your authenticator, or a backup code."
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
msgid "Please review the document before approving."
msgstr "Please review the document before approving."
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx #: apps/remix/app/components/general/document-signing/document-signing-form.tsx
msgid "Please review the document before signing." msgid "Please review the document before signing."
msgstr "Please review the document before signing." msgstr "Please review the document before signing."

View File

@@ -473,6 +473,7 @@ msgstr "<0>Está a punto de completar la aprobación de <1>\"{documentTitle}\"</
msgid "<0>You are about to complete signing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?" msgid "<0>You are about to complete signing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?"
msgstr "<0>Está a punto de completar la firma de \"<1>{documentTitle}</1>\".</0><2/> ¿Está seguro?" msgstr "<0>Está a punto de completar la firma de \"<1>{documentTitle}</1>\".</0><2/> ¿Está seguro?"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "<0>You are about to complete viewing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?" msgid "<0>You are about to complete viewing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?"
msgstr "<0>Está a punto de completar la visualización de \"<1>{documentTitle}</1>\".</0><2/> ¿Está seguro?" msgstr "<0>Está a punto de completar la visualización de \"<1>{documentTitle}</1>\".</0><2/> ¿Está seguro?"
@@ -1144,6 +1145,7 @@ msgstr "Versión de la Aplicación"
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/tables/documents-table-action-button.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-button.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx
#: packages/lib/constants/recipient-roles.ts #: packages/lib/constants/recipient-roles.ts
msgid "Approve" msgid "Approve"
@@ -1565,6 +1567,7 @@ msgstr "Haga clic para insertar campo"
msgid "Close" msgid "Close"
msgstr "Cerrar" msgstr "Cerrar"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/embed/embed-document-signing-page.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
@@ -1576,6 +1579,10 @@ msgstr "Completo"
msgid "Complete Approval" msgid "Complete Approval"
msgstr "Completar Aprobación" msgstr "Completar Aprobación"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Complete Assisting"
msgstr ""
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx #: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Complete Document" msgid "Complete Document"
msgstr "" msgstr ""
@@ -1588,6 +1595,7 @@ msgstr "Completar Firmado"
msgid "Complete the fields for the following signers. Once reviewed, they will inform you if any modifications are needed." msgid "Complete the fields for the following signers. Once reviewed, they will inform you if any modifications are needed."
msgstr "" msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Complete Viewing" msgid "Complete Viewing"
msgstr "Completar Visualización" msgstr "Completar Visualización"
@@ -3373,6 +3381,11 @@ msgstr "Gestionar la configuración de tu sitio aquí"
msgid "Manager" msgid "Manager"
msgstr "Gerente" msgstr "Gerente"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Mark as viewed"
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Mark as Viewed" msgid "Mark as Viewed"
msgstr "Marcar como visto" msgstr "Marcar como visto"
@@ -3988,6 +4001,10 @@ msgstr "Por favor, proporciona un token del autenticador o un código de respald
msgid "Please provide a token from your authenticator, or a backup code." msgid "Please provide a token from your authenticator, or a backup code."
msgstr "Por favor, proporciona un token de tu autenticador, o un código de respaldo." msgstr "Por favor, proporciona un token de tu autenticador, o un código de respaldo."
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
msgid "Please review the document before approving."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx #: apps/remix/app/components/general/document-signing/document-signing-form.tsx
msgid "Please review the document before signing." msgid "Please review the document before signing."
msgstr "Por favor, revise el documento antes de firmar." msgstr "Por favor, revise el documento antes de firmar."

View File

@@ -473,6 +473,7 @@ msgstr "<0>Vous êtes sur le point de terminer l'approbation de <1>\"{documentTi
msgid "<0>You are about to complete signing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?" msgid "<0>You are about to complete signing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?"
msgstr "<0>Vous êtes sur le point de terminer la signature de \"<1>{documentTitle}</1>\".</0><2/> Êtes-vous sûr ?" msgstr "<0>Vous êtes sur le point de terminer la signature de \"<1>{documentTitle}</1>\".</0><2/> Êtes-vous sûr ?"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "<0>You are about to complete viewing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?" msgid "<0>You are about to complete viewing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?"
msgstr "<0>Vous êtes sur le point de terminer la visualisation de \"<1>{documentTitle}</1>\".</0><2/> Êtes-vous sûr ?" msgstr "<0>Vous êtes sur le point de terminer la visualisation de \"<1>{documentTitle}</1>\".</0><2/> Êtes-vous sûr ?"
@@ -1144,6 +1145,7 @@ msgstr "Version de l'application"
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/tables/documents-table-action-button.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-button.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx
#: packages/lib/constants/recipient-roles.ts #: packages/lib/constants/recipient-roles.ts
msgid "Approve" msgid "Approve"
@@ -1565,6 +1567,7 @@ msgstr "Cliquez pour insérer un champ"
msgid "Close" msgid "Close"
msgstr "Fermer" msgstr "Fermer"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/embed/embed-document-signing-page.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
@@ -1576,6 +1579,10 @@ msgstr "Compléter"
msgid "Complete Approval" msgid "Complete Approval"
msgstr "Compléter l'approbation" msgstr "Compléter l'approbation"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Complete Assisting"
msgstr ""
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx #: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Complete Document" msgid "Complete Document"
msgstr "" msgstr ""
@@ -1588,6 +1595,7 @@ msgstr "Compléter la signature"
msgid "Complete the fields for the following signers. Once reviewed, they will inform you if any modifications are needed." msgid "Complete the fields for the following signers. Once reviewed, they will inform you if any modifications are needed."
msgstr "" msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Complete Viewing" msgid "Complete Viewing"
msgstr "Compléter la visualisation" msgstr "Compléter la visualisation"
@@ -3373,6 +3381,11 @@ msgstr "Gérer les paramètres de votre site ici"
msgid "Manager" msgid "Manager"
msgstr "Gestionnaire" msgstr "Gestionnaire"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Mark as viewed"
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Mark as Viewed" msgid "Mark as Viewed"
msgstr "Marquer comme vu" msgstr "Marquer comme vu"
@@ -3988,6 +4001,10 @@ msgstr "Veuillez fournir un token de l'authentificateur, ou un code de secours.
msgid "Please provide a token from your authenticator, or a backup code." msgid "Please provide a token from your authenticator, or a backup code."
msgstr "Veuillez fournir un token de votre authentificateur, ou un code de secours." msgstr "Veuillez fournir un token de votre authentificateur, ou un code de secours."
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
msgid "Please review the document before approving."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx #: apps/remix/app/components/general/document-signing/document-signing-form.tsx
msgid "Please review the document before signing." msgid "Please review the document before signing."
msgstr "Veuillez examiner le document avant de signer." msgstr "Veuillez examiner le document avant de signer."

View File

@@ -473,6 +473,7 @@ msgstr "<0>Stai per completare l'approvazione di <1>\"{documentTitle}\"</1>.</0>
msgid "<0>You are about to complete signing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?" msgid "<0>You are about to complete signing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?"
msgstr "<0>Stai per completare la firma di \"<1>{documentTitle}</1>\".</0><2/> Sei sicuro?" msgstr "<0>Stai per completare la firma di \"<1>{documentTitle}</1>\".</0><2/> Sei sicuro?"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "<0>You are about to complete viewing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?" msgid "<0>You are about to complete viewing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?"
msgstr "<0>Stai per completare la visualizzazione di \"<1>{documentTitle}</1>\".</0><2/> Sei sicuro?" msgstr "<0>Stai per completare la visualizzazione di \"<1>{documentTitle}</1>\".</0><2/> Sei sicuro?"
@@ -1144,6 +1145,7 @@ msgstr "Versione dell'app"
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/tables/documents-table-action-button.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-button.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx
#: packages/lib/constants/recipient-roles.ts #: packages/lib/constants/recipient-roles.ts
msgid "Approve" msgid "Approve"
@@ -1565,6 +1567,7 @@ msgstr "Clicca per inserire il campo"
msgid "Close" msgid "Close"
msgstr "Chiudi" msgstr "Chiudi"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/embed/embed-document-signing-page.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
@@ -1576,6 +1579,10 @@ msgstr "Completa"
msgid "Complete Approval" msgid "Complete Approval"
msgstr "Completa l'Approvazione" msgstr "Completa l'Approvazione"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Complete Assisting"
msgstr ""
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx #: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Complete Document" msgid "Complete Document"
msgstr "" msgstr ""
@@ -1588,6 +1595,7 @@ msgstr "Completa la Firma"
msgid "Complete the fields for the following signers. Once reviewed, they will inform you if any modifications are needed." msgid "Complete the fields for the following signers. Once reviewed, they will inform you if any modifications are needed."
msgstr "" msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Complete Viewing" msgid "Complete Viewing"
msgstr "Completa la Visualizzazione" msgstr "Completa la Visualizzazione"
@@ -3373,6 +3381,11 @@ msgstr "Gestisci le impostazioni del tuo sito qui"
msgid "Manager" msgid "Manager"
msgstr "Responsabile" msgstr "Responsabile"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Mark as viewed"
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Mark as Viewed" msgid "Mark as Viewed"
msgstr "Segna come visto" msgstr "Segna come visto"
@@ -3988,6 +4001,10 @@ msgstr "Si prega di fornire un token dal tuo autenticatore, o un codice di backu
msgid "Please provide a token from your authenticator, or a backup code." msgid "Please provide a token from your authenticator, or a backup code."
msgstr "Si prega di fornire un token dal tuo autenticatore, o un codice di backup." msgstr "Si prega di fornire un token dal tuo autenticatore, o un codice di backup."
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
msgid "Please review the document before approving."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx #: apps/remix/app/components/general/document-signing/document-signing-form.tsx
msgid "Please review the document before signing." msgid "Please review the document before signing."
msgstr "Rivedi il documento prima di firmare." msgstr "Rivedi il documento prima di firmare."

View File

@@ -473,6 +473,7 @@ msgstr "<0>Jesteś na drodze do zatwierdzenia <1>\"{documentTitle}\"</1>.</0><2/
msgid "<0>You are about to complete signing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?" msgid "<0>You are about to complete signing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?"
msgstr "<0>Jesteś na drodze do ukończenia podpisywania \"<1>{documentTitle}</1>\".</0><2/> Czy jesteś pewien?" msgstr "<0>Jesteś na drodze do ukończenia podpisywania \"<1>{documentTitle}</1>\".</0><2/> Czy jesteś pewien?"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "<0>You are about to complete viewing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?" msgid "<0>You are about to complete viewing \"<1>{documentTitle}</1>\".</0><2/> Are you sure?"
msgstr "<0>Jesteś na drodze do zakończenia przeglądania \"<1>{documentTitle}</1>\".</0><2/> Czy jesteś pewien?" msgstr "<0>Jesteś na drodze do zakończenia przeglądania \"<1>{documentTitle}</1>\".</0><2/> Czy jesteś pewien?"
@@ -1144,6 +1145,7 @@ msgstr "Wersja aplikacji"
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/tables/documents-table-action-button.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-button.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx
#: packages/lib/constants/recipient-roles.ts #: packages/lib/constants/recipient-roles.ts
msgid "Approve" msgid "Approve"
@@ -1565,6 +1567,7 @@ msgstr "Kliknij, aby wstawić pole"
msgid "Close" msgid "Close"
msgstr "Zamknij" msgstr "Zamknij"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/embed/embed-document-signing-page.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
@@ -1576,6 +1579,10 @@ msgstr "Zakończono"
msgid "Complete Approval" msgid "Complete Approval"
msgstr "Zakończ zatwierdzanie" msgstr "Zakończ zatwierdzanie"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Complete Assisting"
msgstr ""
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx #: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Complete Document" msgid "Complete Document"
msgstr "" msgstr ""
@@ -1588,6 +1595,7 @@ msgstr "Zakończ podpisywanie"
msgid "Complete the fields for the following signers. Once reviewed, they will inform you if any modifications are needed." msgid "Complete the fields for the following signers. Once reviewed, they will inform you if any modifications are needed."
msgstr "" msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Complete Viewing" msgid "Complete Viewing"
msgstr "Zakończ wyświetlanie" msgstr "Zakończ wyświetlanie"
@@ -3373,6 +3381,11 @@ msgstr "Zarządzaj ustawieniami swojej witryny tutaj"
msgid "Manager" msgid "Manager"
msgstr "Menedżer" msgstr "Menedżer"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Mark as viewed"
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Mark as Viewed" msgid "Mark as Viewed"
msgstr "Oznacz jako wyświetlone" msgstr "Oznacz jako wyświetlone"
@@ -3988,6 +4001,10 @@ msgstr "Proszę podać token z aplikacji uwierzytelniającej lub kod zapasowy. J
msgid "Please provide a token from your authenticator, or a backup code." msgid "Please provide a token from your authenticator, or a backup code."
msgstr "Proszę podać token z Twojego uwierzytelniacza lub kod zapasowy." msgstr "Proszę podać token z Twojego uwierzytelniacza lub kod zapasowy."
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
msgid "Please review the document before approving."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx #: apps/remix/app/components/general/document-signing/document-signing-form.tsx
msgid "Please review the document before signing." msgid "Please review the document before signing."
msgstr "Proszę przejrzeć dokument przed podpisaniem." msgstr "Proszę przejrzeć dokument przed podpisaniem."

View File

@@ -122,6 +122,44 @@ export const ZFieldMetaNotOptionalSchema = z.discriminatedUnion('type', [
export type TFieldMetaNotOptionalSchema = z.infer<typeof ZFieldMetaNotOptionalSchema>; export type TFieldMetaNotOptionalSchema = z.infer<typeof ZFieldMetaNotOptionalSchema>;
export const ZFieldMetaPrefillFieldsSchema = z
.object({
id: z.number(),
})
.and(
z.discriminatedUnion('type', [
z.object({
type: z.literal('text'),
label: z.string().optional(),
placeholder: z.string().optional(),
value: z.string().optional(),
}),
z.object({
type: z.literal('number'),
label: z.string().optional(),
placeholder: z.string().optional(),
value: z.string().optional(),
}),
z.object({
type: z.literal('radio'),
label: z.string().optional(),
value: z.string().optional(),
}),
z.object({
type: z.literal('checkbox'),
label: z.string().optional(),
value: z.array(z.string()).optional(),
}),
z.object({
type: z.literal('dropdown'),
label: z.string().optional(),
value: z.string().optional(),
}),
]),
);
export type TFieldMetaPrefillFieldsSchema = z.infer<typeof ZFieldMetaPrefillFieldsSchema>;
export const ZFieldMetaSchema = z export const ZFieldMetaSchema = z
.union([ .union([
// Handles an empty object being provided as fieldMeta. // Handles an empty object being provided as fieldMeta.

View File

@@ -451,7 +451,7 @@ export const fieldRouter = router({
return await signFieldWithToken({ return await signFieldWithToken({
token, token,
fieldId, fieldId,
value, value: value ?? '',
isBase64, isBase64,
userId: ctx.user?.id, userId: ctx.user?.id,
authOptions, authOptions,

View File

@@ -153,7 +153,7 @@ export const ZSetFieldsForTemplateResponseSchema = z.object({
export const ZSignFieldWithTokenMutationSchema = z.object({ export const ZSignFieldWithTokenMutationSchema = z.object({
token: z.string(), token: z.string(),
fieldId: z.number(), fieldId: z.number(),
value: z.string().trim(), value: z.string().trim().optional(),
isBase64: z.boolean().optional(), isBase64: z.boolean().optional(),
authOptions: ZRecipientActionAuthSchema.optional(), authOptions: ZRecipientActionAuthSchema.optional(),
}); });

View File

@@ -227,7 +227,8 @@ export const templateRouter = router({
.output(ZCreateDocumentFromTemplateResponseSchema) .output(ZCreateDocumentFromTemplateResponseSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const { teamId } = ctx; const { teamId } = ctx;
const { templateId, recipients, distributeDocument, customDocumentDataId } = input; const { templateId, recipients, distributeDocument, customDocumentDataId, prefillFields } =
input;
const limits = await getServerLimits({ email: ctx.user.email, teamId }); const limits = await getServerLimits({ email: ctx.user.email, teamId });
@@ -242,6 +243,7 @@ export const templateRouter = router({
recipients, recipients,
customDocumentDataId, customDocumentDataId,
requestMetadata: ctx.metadata, requestMetadata: ctx.metadata,
prefillFields,
}); });
if (distributeDocument) { if (distributeDocument) {

View File

@@ -7,6 +7,7 @@ import {
ZDocumentActionAuthTypesSchema, ZDocumentActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth'; } from '@documenso/lib/types/document-auth';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZFieldMetaPrefillFieldsSchema } from '@documenso/lib/types/field-meta';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params'; import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import { import {
ZTemplateLiteSchema, ZTemplateLiteSchema,
@@ -67,6 +68,7 @@ export const ZCreateDocumentFromTemplateRequestSchema = z.object({
'The data ID of an alternative PDF to use when creating the document. If not provided, the PDF attached to the template will be used.', 'The data ID of an alternative PDF to use when creating the document. If not provided, the PDF attached to the template will be used.',
) )
.optional(), .optional(),
prefillFields: z.array(ZFieldMetaPrefillFieldsSchema).optional(),
}); });
export const ZCreateDocumentFromTemplateResponseSchema = ZDocumentSchema; export const ZCreateDocumentFromTemplateResponseSchema = ZDocumentSchema;

View File

@@ -3,8 +3,9 @@ import { useCallback, useEffect, useMemo, useRef, 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 { Prisma } from '@prisma/client';
import type { Field, Recipient } from '@prisma/client'; import type { Field, Recipient } from '@prisma/client';
import { FieldType, Prisma, RecipientRole, SendStatus } from '@prisma/client'; import { FieldType, RecipientRole, SendStatus } from '@prisma/client';
import { import {
CalendarDays, CalendarDays,
Check, Check,
@@ -410,11 +411,8 @@ export const AddFieldsFormPartial = ({
); );
const onFieldCopy = useCallback( const onFieldCopy = useCallback(
( (event?: KeyboardEvent | null, options?: { duplicate?: boolean }) => {
event?: KeyboardEvent | null, const { duplicate = false } = options ?? {};
options?: { duplicate?: boolean; duplicateAllPages?: boolean },
) => {
const { duplicate = false, duplicateAllPages = false } = options ?? {};
if (lastActiveField) { if (lastActiveField) {
event?.preventDefault(); event?.preventDefault();
@@ -430,31 +428,6 @@ export const AddFieldsFormPartial = ({
return; return;
} }
if (duplicateAllPages) {
const pages = Array.from(document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR));
const totalPages = pages.length;
for (let pageNumber = 1; pageNumber <= totalPages; pageNumber++) {
if (pageNumber !== lastActiveField.pageNumber) {
const newField: TAddFieldsFormSchema['fields'][0] = {
...structuredClone(lastActiveField),
formId: nanoid(12),
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
pageNumber,
};
append(newField);
}
}
toast({
title: 'Field duplicated',
description: 'Field has been duplicated across all pages',
});
return;
}
const newField: TAddFieldsFormSchema['fields'][0] = { const newField: TAddFieldsFormSchema['fields'][0] = {
...structuredClone(lastActiveField), ...structuredClone(lastActiveField),
formId: nanoid(12), formId: nanoid(12),
@@ -680,9 +653,6 @@ export const AddFieldsFormPartial = ({
onMove={(options) => onFieldMove(options, index)} onMove={(options) => onFieldMove(options, index)}
onRemove={() => remove(index)} onRemove={() => remove(index)}
onDuplicate={() => onFieldCopy(null, { duplicate: true })} onDuplicate={() => onFieldCopy(null, { duplicate: true })}
onDuplicateAllPages={() =>
onFieldCopy(null, { duplicate: true, duplicateAllPages: true })
}
onAdvancedSettings={() => { onAdvancedSettings={() => {
setCurrentField(field); setCurrentField(field);
handleAdvancedSettings(); handleAdvancedSettings();

View File

@@ -303,7 +303,6 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
/> />
)) ))
.otherwise(() => null)} .otherwise(() => null)}
{errors.length > 0 && ( {errors.length > 0 && (
<div className="mt-4"> <div className="mt-4">
<ul> <ul>
@@ -316,7 +315,6 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
</div> </div>
)} )}
</DocumentFlowFormContainerContent> </DocumentFlowFormContainerContent>
<DocumentFlowFormContainerFooter className="mt-auto"> <DocumentFlowFormContainerFooter className="mt-auto">
<DocumentFlowFormContainerActions <DocumentFlowFormContainerActions
goNextLabel={msg`Save`} goNextLabel={msg`Save`}

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FieldType } from '@prisma/client'; import { FieldType } from '@prisma/client';
import { Copy, CopyPlus, Settings2, Trash } from 'lucide-react'; import { CopyPlus, Settings2, Trash } from 'lucide-react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { Rnd } from 'react-rnd'; import { Rnd } from 'react-rnd';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
@@ -31,7 +31,6 @@ export type FieldItemProps = {
onMove?: (_node: HTMLElement) => void; onMove?: (_node: HTMLElement) => void;
onRemove?: () => void; onRemove?: () => void;
onDuplicate?: () => void; onDuplicate?: () => void;
onDuplicateAllPages?: () => void;
onAdvancedSettings?: () => void; onAdvancedSettings?: () => void;
onFocus?: () => void; onFocus?: () => void;
onBlur?: () => void; onBlur?: () => void;
@@ -55,10 +54,9 @@ export const FieldItem = ({
onMove, onMove,
onRemove, onRemove,
onDuplicate, onDuplicate,
onDuplicateAllPages,
onAdvancedSettings,
onFocus, onFocus,
onBlur, onBlur,
onAdvancedSettings,
recipientIndex = 0, recipientIndex = 0,
hideRecipients = false, hideRecipients = false,
hasErrors, hasErrors,
@@ -327,14 +325,6 @@ export const FieldItem = ({
<CopyPlus className="h-3 w-3" /> <CopyPlus className="h-3 w-3" />
</button> </button>
<button
className="dark:text-muted-foreground/50 dark:hover:text-muted-foreground dark:hover:bg-foreground/10 rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
onClick={onDuplicateAllPages}
onTouchEnd={onDuplicateAllPages}
>
<Copy className="h-3 w-3" />
</button>
<button <button
className="dark:text-muted-foreground/50 dark:hover:text-muted-foreground dark:hover:bg-foreground/10 rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100" className="dark:text-muted-foreground/50 dark:hover:text-muted-foreground dark:hover:bg-foreground/10 rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
onClick={onRemove} onClick={onRemove}

View File

@@ -104,8 +104,12 @@ export const DropdownFieldAdvancedSettings = ({
<Trans>Select default option</Trans> <Trans>Select default option</Trans>
</Label> </Label>
<Select <Select
defaultValue={defaultValue} value={defaultValue}
onValueChange={(val) => { onValueChange={(val) => {
if (!val) {
return;
}
setDefaultValue(val); setDefaultValue(val);
handleFieldChange('defaultValue', val); handleFieldChange('defaultValue', val);
}} }}

View File

@@ -127,7 +127,13 @@ export const TextFieldAdvancedSettings = ({
<Select <Select
value={fieldState.textAlign} value={fieldState.textAlign}
onValueChange={(value) => handleInput('textAlign', value)} onValueChange={(value) => {
if (!value) {
return;
}
handleInput('textAlign', value);
}}
> >
<SelectTrigger className="bg-background mt-2"> <SelectTrigger className="bg-background mt-2">
<SelectValue placeholder="Select text align" /> <SelectValue placeholder="Select text align" />