Compare commits
3 Commits
v1.10.0-rc
...
feat/limit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6fbd84027 | ||
|
|
fabb2fc3d2 | ||
|
|
2d4b18b1e8 |
@@ -52,9 +52,9 @@ Platform customers have access to advanced styling options to customize the embe
|
||||
<EmbedDirectTemplate
|
||||
token={token}
|
||||
cssVars={{
|
||||
primary: '#0000FF',
|
||||
background: '#F5F5F5',
|
||||
radius: '8px',
|
||||
colorPrimary: '#0000FF',
|
||||
colorBackground: '#F5F5F5',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
@@ -95,9 +95,9 @@ const MyEmbeddingComponent = () => {
|
||||
}
|
||||
`;
|
||||
const cssVars = {
|
||||
primary: '#0000FF',
|
||||
background: '#F5F5F5',
|
||||
radius: '8px',
|
||||
colorPrimary: '#0000FF',
|
||||
colorBackground: '#F5F5F5',
|
||||
borderRadius: '8px',
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -99,9 +99,9 @@ const MyEmbeddingComponent = () => {
|
||||
`}
|
||||
// CSS Variables
|
||||
cssVars={{
|
||||
primary: '#0000FF',
|
||||
background: '#F5F5F5',
|
||||
radius: '8px',
|
||||
colorPrimary: '#0000FF',
|
||||
colorBackground: '#F5F5F5',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
// Dark Mode Control
|
||||
darkModeDisabled={true}
|
||||
|
||||
@@ -95,9 +95,9 @@ const MyEmbeddingComponent = () => {
|
||||
}
|
||||
`;
|
||||
const cssVars = {
|
||||
primary: '#0000FF',
|
||||
background: '#F5F5F5',
|
||||
radius: '8px',
|
||||
colorPrimary: '#0000FF',
|
||||
colorBackground: '#F5F5F5',
|
||||
borderRadius: '8px',
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -97,9 +97,9 @@ Platform customers have access to advanced styling options:
|
||||
}
|
||||
`;
|
||||
const cssVars = {
|
||||
primary: '#0000FF',
|
||||
background: '#F5F5F5',
|
||||
radius: '8px',
|
||||
colorPrimary: '#0000FF',
|
||||
colorBackground: '#F5F5F5',
|
||||
borderRadius: '8px',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -97,9 +97,9 @@ Platform customers have access to advanced styling options:
|
||||
}
|
||||
`;
|
||||
const cssVars = {
|
||||
primary: '#0000FF',
|
||||
background: '#F5F5F5',
|
||||
radius: '8px',
|
||||
colorPrimary: '#0000FF',
|
||||
colorBackground: '#F5F5F5',
|
||||
borderRadius: '8px',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
@@ -14,208 +9,64 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
|
||||
import { DocumentSigningDisclosure } from '../general/document-signing/document-signing-disclosure';
|
||||
|
||||
export type NextSigner = {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
type ConfirmationDialogProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: (nextSigner?: NextSigner) => void;
|
||||
onConfirm: () => void;
|
||||
hasUninsertedFields: boolean;
|
||||
isSubmitting: boolean;
|
||||
allowDictateNextSigner?: boolean;
|
||||
defaultNextSigner?: NextSigner;
|
||||
};
|
||||
|
||||
const ZNextSignerFormSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
email: z.string().email('Invalid email address'),
|
||||
});
|
||||
|
||||
type TNextSignerFormSchema = z.infer<typeof ZNextSignerFormSchema>;
|
||||
|
||||
export function AssistantConfirmationDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
hasUninsertedFields,
|
||||
isSubmitting,
|
||||
allowDictateNextSigner = false,
|
||||
defaultNextSigner,
|
||||
}: ConfirmationDialogProps) {
|
||||
const [isEditingNextSigner, setIsEditingNextSigner] = useState(false);
|
||||
|
||||
const form = useForm<TNextSignerFormSchema>({
|
||||
resolver: zodResolver(ZNextSignerFormSchema),
|
||||
defaultValues: {
|
||||
name: defaultNextSigner?.name ?? '',
|
||||
email: defaultNextSigner?.email ?? '',
|
||||
},
|
||||
});
|
||||
|
||||
const onOpenChange = () => {
|
||||
if (form.formState.isSubmitting) {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.reset({
|
||||
name: defaultNextSigner?.name ?? '',
|
||||
email: defaultNextSigner?.email ?? '',
|
||||
});
|
||||
|
||||
setIsEditingNextSigner(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onFormSubmit = async (data: TNextSignerFormSchema) => {
|
||||
if (allowDictateNextSigner && data.name && data.email) {
|
||||
await onConfirm({
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
});
|
||||
} else {
|
||||
await onConfirm();
|
||||
}
|
||||
};
|
||||
|
||||
const isNextSignerValid = !allowDictateNextSigner || (form.watch('name') && form.watch('email'));
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||
<fieldset
|
||||
disabled={form.formState.isSubmitting || isSubmitting}
|
||||
className="border-none p-0"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Complete Document</Trans>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
Are you sure you want to complete the document? This action cannot be undone.
|
||||
Please ensure that you have completed prefilling all relevant fields before
|
||||
proceeding.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Complete Document</Trans>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
Are you sure you want to complete the document? This action cannot be undone. Please
|
||||
ensure that you have completed prefilling all relevant fields before proceeding.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-4">
|
||||
{allowDictateNextSigner && (
|
||||
<div className="space-y-4">
|
||||
{!isEditingNextSigner && (
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
The next recipient to sign this document will be{' '}
|
||||
<span className="font-semibold">{form.watch('name')}</span> (
|
||||
<span className="font-semibold">{form.watch('email')}</span>).
|
||||
</p>
|
||||
<div className="flex flex-col gap-4">
|
||||
<DocumentSigningDisclosure />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
className="mt-2"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsEditingNextSigner((prev) => !prev)}
|
||||
>
|
||||
<Trans>Update Recipient</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isEditingNextSigner && (
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Name</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
className="mt-2"
|
||||
placeholder="Enter the next signer's name"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Email</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="email"
|
||||
className="mt-2"
|
||||
placeholder="Enter the next signer's email"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DocumentSigningDisclosure className="mt-4" />
|
||||
</div>
|
||||
|
||||
<DialogFooter className="mt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={onClose}
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant={hasUninsertedFields ? 'destructive' : 'default'}
|
||||
disabled={form.formState.isSubmitting || !isNextSignerValid}
|
||||
loading={form.formState.isSubmitting}
|
||||
>
|
||||
{form.formState.isSubmitting ? (
|
||||
<Trans>Submitting...</Trans>
|
||||
) : hasUninsertedFields ? (
|
||||
<Trans>Proceed</Trans>
|
||||
) : (
|
||||
<Trans>Continue</Trans>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
<DialogFooter className="mt-4">
|
||||
<Button variant="secondary" onClick={onClose} disabled={isSubmitting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant={hasUninsertedFields ? 'destructive' : 'default'}
|
||||
onClick={onConfirm}
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Submitting...' : hasUninsertedFields ? 'Proceed' : 'Continue'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
import { P, match } from 'ts-pattern';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||
@@ -146,7 +146,7 @@ export const DocumentDeleteDialog = ({
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
))
|
||||
.with(P.union(DocumentStatus.COMPLETED, DocumentStatus.REJECTED), () => (
|
||||
.with(DocumentStatus.COMPLETED, () => (
|
||||
<AlertDescription>
|
||||
<p>
|
||||
<Trans>By deleting this document, the following will occur:</Trans>
|
||||
|
||||
@@ -13,10 +13,6 @@ import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'
|
||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
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 { trpc } from '@documenso/trpc/react';
|
||||
import type {
|
||||
@@ -96,7 +92,7 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(() => fields);
|
||||
|
||||
const [pendingFields, _completedFields] = [
|
||||
localFields.filter((field) => isFieldUnsignedAndRequired(field)),
|
||||
localFields.filter((field) => !field.inserted),
|
||||
localFields.filter((field) => field.inserted),
|
||||
];
|
||||
|
||||
@@ -114,7 +110,7 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
|
||||
const newField: DirectTemplateLocalField = structuredClone({
|
||||
...field,
|
||||
customText: payload.value ?? '',
|
||||
customText: payload.value,
|
||||
inserted: true,
|
||||
signedValue: payload,
|
||||
});
|
||||
@@ -125,10 +121,8 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
created: new Date(),
|
||||
recipientId: 1,
|
||||
fieldId: 1,
|
||||
signatureImageAsBase64:
|
||||
payload.value && payload.value.startsWith('data:') ? payload.value : null,
|
||||
typedSignature:
|
||||
payload.value && !payload.value.startsWith('data:') ? payload.value : null,
|
||||
signatureImageAsBase64: payload.value.startsWith('data:') ? payload.value : null,
|
||||
typedSignature: payload.value.startsWith('data:') ? null : payload.value,
|
||||
} satisfies Signature;
|
||||
}
|
||||
|
||||
@@ -186,7 +180,7 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
};
|
||||
|
||||
const onNextFieldClick = () => {
|
||||
validateFieldsInserted(pendingFields);
|
||||
validateFieldsInserted(localFields);
|
||||
|
||||
setShowPendingFieldTooltip(true);
|
||||
setIsExpanded(false);
|
||||
@@ -198,7 +192,7 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const valid = validateFieldsInserted(pendingFields);
|
||||
const valid = validateFieldsInserted(localFields);
|
||||
|
||||
if (!valid) {
|
||||
setShowPendingFieldTooltip(true);
|
||||
@@ -211,6 +205,12 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
directTemplateExternalId = decodeURIComponent(directTemplateExternalId);
|
||||
}
|
||||
|
||||
localFields.forEach((field) => {
|
||||
if (!field.signedValue) {
|
||||
throw new Error('Invalid configuration');
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
documentId,
|
||||
token: documentToken,
|
||||
@@ -221,11 +221,13 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
directRecipientName: fullName,
|
||||
directRecipientEmail: email,
|
||||
templateUpdatedAt: updatedAt,
|
||||
signedFieldValues: localFields
|
||||
.filter((field) => {
|
||||
return field.signedValue && (isRequiredField(field) || field.inserted);
|
||||
})
|
||||
.map((field) => field.signedValue!),
|
||||
signedFieldValues: localFields.map((field) => {
|
||||
if (!field.signedValue) {
|
||||
throw new Error('Invalid configuration');
|
||||
}
|
||||
|
||||
return field.signedValue;
|
||||
}),
|
||||
});
|
||||
|
||||
if (window.parent) {
|
||||
@@ -413,42 +415,40 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{hasSignatureField && (
|
||||
<div>
|
||||
<Label htmlFor="Signature">
|
||||
<Trans>Signature</Trans>
|
||||
</Label>
|
||||
<div>
|
||||
<Label htmlFor="Signature">
|
||||
<Trans>Signature</Trans>
|
||||
</Label>
|
||||
|
||||
<Card className="mt-2" gradient degrees={-120}>
|
||||
<CardContent className="p-0">
|
||||
<SignaturePad
|
||||
className="h-44 w-full"
|
||||
disabled={isThrottled || isSubmitting}
|
||||
defaultValue={signature ?? undefined}
|
||||
onChange={(value) => {
|
||||
setSignature(value);
|
||||
}}
|
||||
onValidityChange={(isValid) => {
|
||||
setSignatureValid(isValid);
|
||||
}}
|
||||
allowTypedSignature={Boolean(
|
||||
metadata &&
|
||||
'typedSignatureEnabled' in metadata &&
|
||||
metadata.typedSignatureEnabled,
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="mt-2" gradient degrees={-120}>
|
||||
<CardContent className="p-0">
|
||||
<SignaturePad
|
||||
className="h-44 w-full"
|
||||
disabled={isThrottled || isSubmitting}
|
||||
defaultValue={signature ?? undefined}
|
||||
onChange={(value) => {
|
||||
setSignature(value);
|
||||
}}
|
||||
onValidityChange={(isValid) => {
|
||||
setSignatureValid(isValid);
|
||||
}}
|
||||
allowTypedSignature={Boolean(
|
||||
metadata &&
|
||||
'typedSignatureEnabled' in metadata &&
|
||||
metadata.typedSignatureEnabled,
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{hasSignatureField && !signatureValid && (
|
||||
<div className="text-destructive mt-2 text-sm">
|
||||
<Trans>
|
||||
Signature is too small. Please provide a more complete signature.
|
||||
</Trans>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{hasSignatureField && !signatureValid && (
|
||||
<div className="text-destructive mt-2 text-sm">
|
||||
<Trans>
|
||||
Signature is too small. Please provide a more complete signature.
|
||||
</Trans>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useId, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useId, useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
@@ -15,7 +15,6 @@ import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||
|
||||
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||
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 type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
@@ -102,26 +101,19 @@ export const EmbedSignDocumentClientPage = ({
|
||||
const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500);
|
||||
|
||||
const [pendingFields, _completedFields] = [
|
||||
fields.filter(
|
||||
(field) => field.recipientId === recipient.id && isFieldUnsignedAndRequired(field),
|
||||
),
|
||||
fields.filter((field) => field.recipientId === recipient.id && !field.inserted),
|
||||
fields.filter((field) => field.inserted),
|
||||
];
|
||||
|
||||
const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } =
|
||||
trpc.recipient.completeDocumentWithToken.useMutation();
|
||||
|
||||
const fieldsRequiringValidation = useMemo(
|
||||
() => fields.filter(isFieldUnsignedAndRequired),
|
||||
[fields],
|
||||
);
|
||||
|
||||
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
|
||||
|
||||
const assistantSignersId = useId();
|
||||
|
||||
const onNextFieldClick = () => {
|
||||
validateFieldsInserted(fieldsRequiringValidation);
|
||||
validateFieldsInserted(fields);
|
||||
|
||||
setShowPendingFieldTooltip(true);
|
||||
setIsExpanded(false);
|
||||
@@ -133,7 +125,7 @@ export const EmbedSignDocumentClientPage = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const valid = validateFieldsInserted(fieldsRequiringValidation);
|
||||
const valid = validateFieldsInserted(fields);
|
||||
|
||||
if (!valid) {
|
||||
setShowPendingFieldTooltip(true);
|
||||
@@ -426,42 +418,40 @@ export const EmbedSignDocumentClientPage = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{hasSignatureField && (
|
||||
<div>
|
||||
<Label htmlFor="Signature">
|
||||
<Trans>Signature</Trans>
|
||||
</Label>
|
||||
<div>
|
||||
<Label htmlFor="Signature">
|
||||
<Trans>Signature</Trans>
|
||||
</Label>
|
||||
|
||||
<Card className="mt-2" gradient degrees={-120}>
|
||||
<CardContent className="p-0">
|
||||
<SignaturePad
|
||||
className="h-44 w-full"
|
||||
disabled={isThrottled || isSubmitting}
|
||||
defaultValue={signature ?? undefined}
|
||||
onChange={(value) => {
|
||||
setSignature(value);
|
||||
}}
|
||||
onValidityChange={(isValid) => {
|
||||
setSignatureValid(isValid);
|
||||
}}
|
||||
allowTypedSignature={Boolean(
|
||||
metadata &&
|
||||
'typedSignatureEnabled' in metadata &&
|
||||
metadata.typedSignatureEnabled,
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="mt-2" gradient degrees={-120}>
|
||||
<CardContent className="p-0">
|
||||
<SignaturePad
|
||||
className="h-44 w-full"
|
||||
disabled={isThrottled || isSubmitting}
|
||||
defaultValue={signature ?? undefined}
|
||||
onChange={(value) => {
|
||||
setSignature(value);
|
||||
}}
|
||||
onValidityChange={(isValid) => {
|
||||
setSignatureValid(isValid);
|
||||
}}
|
||||
allowTypedSignature={Boolean(
|
||||
metadata &&
|
||||
'typedSignatureEnabled' in metadata &&
|
||||
metadata.typedSignatureEnabled,
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{hasSignatureField && !signatureValid && (
|
||||
<div className="text-destructive mt-2 text-sm">
|
||||
<Trans>
|
||||
Signature is too small. Please provide a more complete signature.
|
||||
</Trans>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{hasSignatureField && !signatureValid && (
|
||||
<div className="text-destructive mt-2 text-sm">
|
||||
<Trans>
|
||||
Signature is too small. Please provide a more complete signature.
|
||||
</Trans>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -531,27 +531,6 @@ export const SignUpForm = ({
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
<p className="text-muted-foreground mt-6 text-xs">
|
||||
<Trans>
|
||||
By proceeding, you agree to our{' '}
|
||||
<Link
|
||||
to="https://documen.so/terms"
|
||||
target="_blank"
|
||||
className="text-documenso-700 duration-200 hover:opacity-70"
|
||||
>
|
||||
Terms of Service
|
||||
</Link>{' '}
|
||||
and{' '}
|
||||
<Link
|
||||
to="https://documen.so/privacy"
|
||||
target="_blank"
|
||||
className="text-documenso-700 duration-200 hover:opacity-70"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -76,7 +76,7 @@ export const AppNavDesktop = ({
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-muted-foreground flex w-full max-w-96 items-center justify-between rounded-lg"
|
||||
className="text-muted-foreground flex w-96 items-center justify-between rounded-lg"
|
||||
onClick={() => setIsCommandMenuOpen(true)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { Field, Recipient, Signature } from '@prisma/client';
|
||||
@@ -91,7 +91,7 @@ export const DirectTemplateSigningForm = ({
|
||||
|
||||
const tempField: DirectTemplateLocalField = {
|
||||
...field,
|
||||
customText: value.value ?? '',
|
||||
customText: value.value,
|
||||
inserted: true,
|
||||
signedValue: value,
|
||||
};
|
||||
@@ -102,8 +102,8 @@ export const DirectTemplateSigningForm = ({
|
||||
created: new Date(),
|
||||
recipientId: 1,
|
||||
fieldId: 1,
|
||||
signatureImageAsBase64: value.value?.startsWith('data:') ? value.value : null,
|
||||
typedSignature: value.value && !value.value.startsWith('data:') ? value.value : null,
|
||||
signatureImageAsBase64: value.value.startsWith('data:') ? value.value : null,
|
||||
typedSignature: value.value.startsWith('data:') ? null : value.value,
|
||||
} satisfies Signature;
|
||||
}
|
||||
|
||||
@@ -170,55 +170,6 @@ export const DirectTemplateSigningForm = ({
|
||||
// Do not reset to false since we do a redirect.
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const updatedFields = [...localFields];
|
||||
|
||||
localFields.forEach((field) => {
|
||||
const index = updatedFields.findIndex((f) => f.id === field.id);
|
||||
let value = '';
|
||||
|
||||
match(field.type)
|
||||
.with(FieldType.TEXT, () => {
|
||||
const meta = field.fieldMeta ? ZTextFieldMeta.safeParse(field.fieldMeta) : null;
|
||||
|
||||
if (meta?.success) {
|
||||
value = meta.data.text ?? '';
|
||||
}
|
||||
})
|
||||
.with(FieldType.NUMBER, () => {
|
||||
const meta = field.fieldMeta ? ZNumberFieldMeta.safeParse(field.fieldMeta) : null;
|
||||
|
||||
if (meta?.success) {
|
||||
value = meta.data.value ?? '';
|
||||
}
|
||||
})
|
||||
.with(FieldType.DROPDOWN, () => {
|
||||
const meta = field.fieldMeta ? ZDropdownFieldMeta.safeParse(field.fieldMeta) : null;
|
||||
|
||||
if (meta?.success) {
|
||||
value = meta.data.defaultValue ?? '';
|
||||
}
|
||||
});
|
||||
|
||||
if (value) {
|
||||
const signedValue = {
|
||||
token: directRecipient.token,
|
||||
fieldId: field.id,
|
||||
value,
|
||||
};
|
||||
|
||||
updatedFields[index] = {
|
||||
...field,
|
||||
customText: value,
|
||||
inserted: true,
|
||||
signedValue,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
setLocalFields(updatedFields);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DocumentSigningRecipientProvider recipient={directRecipient}>
|
||||
<DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} />
|
||||
|
||||
@@ -97,10 +97,6 @@ export const DocumentSigningCheckboxField = ({
|
||||
|
||||
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
||||
try {
|
||||
if (!isLengthConditionMet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: TSignFieldWithTokenMutationSchema = {
|
||||
token: recipient.token,
|
||||
fieldId: field.id,
|
||||
@@ -198,30 +194,18 @@ export const DocumentSigningCheckboxField = ({
|
||||
|
||||
setCheckedValues(updatedValues);
|
||||
|
||||
const removePayload: TRemovedSignedFieldWithTokenMutationSchema = {
|
||||
await removeSignedFieldWithToken({
|
||||
token: recipient.token,
|
||||
fieldId: field.id,
|
||||
};
|
||||
});
|
||||
|
||||
if (onUnsignField) {
|
||||
await onUnsignField(removePayload);
|
||||
} else {
|
||||
await removeSignedFieldWithToken(removePayload);
|
||||
}
|
||||
|
||||
if (updatedValues.length > 0 && shouldAutoSignField) {
|
||||
const signPayload: TSignFieldWithTokenMutationSchema = {
|
||||
if (updatedValues.length > 0) {
|
||||
await signFieldWithToken({
|
||||
token: recipient.token,
|
||||
fieldId: field.id,
|
||||
value: toCheckboxValue(updatedValues),
|
||||
isBase64: true,
|
||||
};
|
||||
|
||||
if (onSignField) {
|
||||
await onSignField(signPayload);
|
||||
} else {
|
||||
await signFieldWithToken(signPayload);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { Field } from '@prisma/client';
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@@ -17,15 +13,6 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
|
||||
import { DocumentSigningDisclosure } from '~/components/general/document-signing/document-signing-disclosure';
|
||||
|
||||
@@ -34,23 +21,11 @@ export type DocumentSigningCompleteDialogProps = {
|
||||
documentTitle: string;
|
||||
fields: Field[];
|
||||
fieldsValidated: () => void | Promise<void>;
|
||||
onSignatureComplete: (nextSigner?: { name: string; email: string }) => void | Promise<void>;
|
||||
onSignatureComplete: () => void | Promise<void>;
|
||||
role: RecipientRole;
|
||||
disabled?: boolean;
|
||||
allowDictateNextSigner?: boolean;
|
||||
defaultNextSigner?: {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
};
|
||||
|
||||
const ZNextSignerFormSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
email: z.string().email('Invalid email address'),
|
||||
});
|
||||
|
||||
type TNextSignerFormSchema = z.infer<typeof ZNextSignerFormSchema>;
|
||||
|
||||
export const DocumentSigningCompleteDialog = ({
|
||||
isSubmitting,
|
||||
documentTitle,
|
||||
@@ -59,54 +34,19 @@ export const DocumentSigningCompleteDialog = ({
|
||||
onSignatureComplete,
|
||||
role,
|
||||
disabled = false,
|
||||
allowDictateNextSigner = false,
|
||||
defaultNextSigner,
|
||||
}: DocumentSigningCompleteDialogProps) => {
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [isEditingNextSigner, setIsEditingNextSigner] = useState(false);
|
||||
|
||||
const form = useForm<TNextSignerFormSchema>({
|
||||
resolver: allowDictateNextSigner ? zodResolver(ZNextSignerFormSchema) : undefined,
|
||||
defaultValues: {
|
||||
name: defaultNextSigner?.name ?? '',
|
||||
email: defaultNextSigner?.email ?? '',
|
||||
},
|
||||
});
|
||||
|
||||
const isComplete = useMemo(() => !fieldsContainUnsignedRequiredField(fields), [fields]);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (form.formState.isSubmitting || !isComplete) {
|
||||
if (isSubmitting || !isComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (open) {
|
||||
form.reset({
|
||||
name: defaultNextSigner?.name ?? '',
|
||||
email: defaultNextSigner?.email ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
setIsEditingNextSigner(false);
|
||||
setShowDialog(open);
|
||||
};
|
||||
|
||||
const onFormSubmit = async (data: TNextSignerFormSchema) => {
|
||||
console.log('data', data);
|
||||
console.log('form.formState.errors', form.formState.errors);
|
||||
try {
|
||||
if (allowDictateNextSigner && data.name && data.email) {
|
||||
await onSignatureComplete({ name: data.name, email: data.email });
|
||||
} else {
|
||||
await onSignatureComplete();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error completing signature:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const isNextSignerValid = !allowDictateNextSigner || (form.watch('name') && form.watch('email'));
|
||||
|
||||
return (
|
||||
<Dialog open={showDialog} onOpenChange={handleOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
@@ -118,196 +58,92 @@ export const DocumentSigningCompleteDialog = ({
|
||||
loading={isSubmitting}
|
||||
disabled={disabled}
|
||||
>
|
||||
{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()}
|
||||
{isComplete ? <Trans>Complete</Trans> : <Trans>Next field</Trans>}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||
<fieldset disabled={form.formState.isSubmitting} className="border-none p-0">
|
||||
<DialogTitle>
|
||||
<div className="text-foreground text-xl font-semibold">
|
||||
{match(role)
|
||||
.with(RecipientRole.VIEWER, () => <Trans>Complete Viewing</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>
|
||||
</DialogTitle>
|
||||
<DialogTitle>
|
||||
<div className="text-foreground text-xl font-semibold">
|
||||
{role === RecipientRole.VIEWER && <Trans>Complete Viewing</Trans>}
|
||||
{role === RecipientRole.SIGNER && <Trans>Complete Signing</Trans>}
|
||||
{role === RecipientRole.APPROVER && <Trans>Complete Approval</Trans>}
|
||||
</div>
|
||||
</DialogTitle>
|
||||
|
||||
<div className="text-muted-foreground max-w-[50ch]">
|
||||
{match(role)
|
||||
.with(RecipientRole.VIEWER, () => (
|
||||
<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>
|
||||
))
|
||||
.with(RecipientRole.SIGNER, () => (
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="inline-flex flex-wrap">
|
||||
You are about to complete signing "
|
||||
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
||||
{documentTitle}
|
||||
</span>
|
||||
".
|
||||
</span>
|
||||
<br /> Are you sure?
|
||||
</Trans>
|
||||
</span>
|
||||
))
|
||||
.with(RecipientRole.APPROVER, () => (
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="inline-flex flex-wrap">
|
||||
You are about to complete approving{' '}
|
||||
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
||||
"{documentTitle}"
|
||||
</span>
|
||||
.
|
||||
</span>
|
||||
<br /> Are you sure?
|
||||
</Trans>
|
||||
</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 className="text-muted-foreground max-w-[50ch]">
|
||||
{role === RecipientRole.VIEWER && (
|
||||
<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>
|
||||
)}
|
||||
{role === RecipientRole.SIGNER && (
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="inline-flex flex-wrap">
|
||||
You are about to complete signing "
|
||||
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
||||
{documentTitle}
|
||||
</span>
|
||||
".
|
||||
</span>
|
||||
<br /> Are you sure?
|
||||
</Trans>
|
||||
</span>
|
||||
)}
|
||||
{role === RecipientRole.APPROVER && (
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="inline-flex flex-wrap">
|
||||
You are about to complete approving{' '}
|
||||
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
||||
"{documentTitle}"
|
||||
</span>
|
||||
.
|
||||
</span>
|
||||
<br /> Are you sure?
|
||||
</Trans>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{allowDictateNextSigner && (
|
||||
<div className="mt-4 flex flex-col gap-4">
|
||||
{!isEditingNextSigner && (
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
The next recipient to sign this document will be{' '}
|
||||
<span className="font-semibold">{form.watch('name')}</span> (
|
||||
<span className="font-semibold">{form.watch('email')}</span>).
|
||||
</p>
|
||||
<DocumentSigningDisclosure className="mt-4" />
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
className="mt-2"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsEditingNextSigner((prev) => !prev)}
|
||||
>
|
||||
<Trans>Update Recipient</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setShowDialog(false);
|
||||
}}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
{isEditingNextSigner && (
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Name</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
className="mt-2"
|
||||
placeholder="Enter the next signer's name"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Email</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="email"
|
||||
className="mt-2"
|
||||
placeholder="Enter the next signer's email"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DocumentSigningDisclosure className="mt-4" />
|
||||
|
||||
<DialogFooter className="mt-4">
|
||||
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
|
||||
variant="secondary"
|
||||
onClick={() => setShowDialog(false)}
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="flex-1"
|
||||
disabled={!isComplete || !isNextSignerValid}
|
||||
loading={form.formState.isSubmitting}
|
||||
>
|
||||
{match(role)
|
||||
.with(RecipientRole.VIEWER, () => <Trans>Mark as Viewed</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>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
<Button
|
||||
type="button"
|
||||
className="flex-1"
|
||||
disabled={!isComplete}
|
||||
loading={isSubmitting}
|
||||
onClick={onSignatureComplete}
|
||||
>
|
||||
{role === RecipientRole.VIEWER && <Trans>Mark as Viewed</Trans>}
|
||||
{role === RecipientRole.SIGNER && <Trans>Sign</Trans>}
|
||||
{role === RecipientRole.APPROVER && <Trans>Approve</Trans>}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useId, useMemo, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { type Field, FieldType, type Recipient, RecipientRole } from '@prisma/client';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
||||
@@ -27,20 +25,10 @@ import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group
|
||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import {
|
||||
AssistantConfirmationDialog,
|
||||
type NextSigner,
|
||||
} from '../../dialogs/assistant-confirmation-dialog';
|
||||
import { AssistantConfirmationDialog } from '../../dialogs/assistant-confirmation-dialog';
|
||||
import { DocumentSigningCompleteDialog } from './document-signing-complete-dialog';
|
||||
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||
|
||||
export const ZSigningFormSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required').optional(),
|
||||
email: z.string().email('Invalid email address').optional(),
|
||||
});
|
||||
|
||||
export type TSigningFormSchema = z.infer<typeof ZSigningFormSchema>;
|
||||
|
||||
export type DocumentSigningFormProps = {
|
||||
document: DocumentAndSender;
|
||||
recipient: Recipient;
|
||||
@@ -87,9 +75,7 @@ export const DocumentSigningForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const { handleSubmit, formState } = useForm<TSigningFormSchema>({
|
||||
resolver: zodResolver(ZSigningFormSchema),
|
||||
});
|
||||
const { handleSubmit, formState } = useForm();
|
||||
|
||||
// Keep the loading state going if successful since the redirect may take some time.
|
||||
const isSubmitting = formState.isSubmitting || formState.isSubmitSuccessful;
|
||||
@@ -114,36 +100,20 @@ export const DocumentSigningForm = ({
|
||||
validateFieldsInserted(fieldsRequiringValidation);
|
||||
};
|
||||
|
||||
const onFormSubmit = async (data: TSigningFormSchema) => {
|
||||
try {
|
||||
setValidateUninsertedFields(true);
|
||||
const onFormSubmit = async () => {
|
||||
setValidateUninsertedFields(true);
|
||||
|
||||
const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation);
|
||||
const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation);
|
||||
|
||||
if (hasSignatureField && !signatureValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isFieldsValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextSigner =
|
||||
data.email && data.name
|
||||
? {
|
||||
email: data.email,
|
||||
name: data.name,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
await completeDocument(undefined, nextSigner);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error instanceof Error ? error.message : 'An error occurred while signing',
|
||||
variant: 'destructive',
|
||||
});
|
||||
if (hasSignatureField && !signatureValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isFieldsValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
await completeDocument();
|
||||
};
|
||||
|
||||
const onAssistantFormSubmit = () => {
|
||||
@@ -154,11 +124,11 @@ export const DocumentSigningForm = ({
|
||||
setIsConfirmationDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleAssistantConfirmDialogSubmit = async (nextSigner?: NextSigner) => {
|
||||
const handleAssistantConfirmDialogSubmit = async () => {
|
||||
setIsAssistantSubmitting(true);
|
||||
|
||||
try {
|
||||
await completeDocument(undefined, nextSigner);
|
||||
await completeDocument();
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
@@ -171,18 +141,12 @@ export const DocumentSigningForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const completeDocument = async (
|
||||
authOptions?: TRecipientActionAuth,
|
||||
nextSigner?: { email: string; name: string },
|
||||
) => {
|
||||
const payload = {
|
||||
const completeDocument = async (authOptions?: TRecipientActionAuth) => {
|
||||
await completeDocumentWithToken({
|
||||
token: recipient.token,
|
||||
documentId: document.id,
|
||||
authOptions,
|
||||
...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}),
|
||||
};
|
||||
|
||||
await completeDocumentWithToken(payload);
|
||||
});
|
||||
|
||||
analytics.capture('App: Recipient has completed signing', {
|
||||
signerId: recipient.id,
|
||||
@@ -197,31 +161,6 @@ export const DocumentSigningForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const nextRecipient = useMemo(() => {
|
||||
if (
|
||||
!document.documentMeta?.signingOrder ||
|
||||
document.documentMeta.signingOrder !== 'SEQUENTIAL'
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sortedRecipients = allRecipients.sort((a, b) => {
|
||||
// Sort by signingOrder first (nulls last), then by id
|
||||
if (a.signingOrder === null && b.signingOrder === null) return a.id - b.id;
|
||||
if (a.signingOrder === null) return 1;
|
||||
if (b.signingOrder === null) return -1;
|
||||
if (a.signingOrder === b.signingOrder) return a.id - b.id;
|
||||
return a.signingOrder - b.signingOrder;
|
||||
});
|
||||
|
||||
const currentIndex = sortedRecipients.findIndex((r) => r.id === recipient.id);
|
||||
return currentIndex !== -1 && currentIndex < sortedRecipients.length - 1
|
||||
? sortedRecipients[currentIndex + 1]
|
||||
: undefined;
|
||||
}, [document.documentMeta?.signingOrder, allRecipients, recipient.id]);
|
||||
|
||||
console.log('nextRecipient', nextRecipient);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -271,19 +210,12 @@ export const DocumentSigningForm = ({
|
||||
|
||||
<DocumentSigningCompleteDialog
|
||||
isSubmitting={isSubmitting}
|
||||
onSignatureComplete={handleSubmit(onFormSubmit)}
|
||||
documentTitle={document.title}
|
||||
fields={fields}
|
||||
fieldsValidated={fieldsValidated}
|
||||
onSignatureComplete={async (nextSigner) => {
|
||||
await completeDocument(undefined, nextSigner);
|
||||
}}
|
||||
role={recipient.role}
|
||||
allowDictateNextSigner={document.documentMeta?.allowDictateNextSigner}
|
||||
defaultNextSigner={
|
||||
nextRecipient
|
||||
? { name: nextRecipient.name, email: nextRecipient.email }
|
||||
: undefined
|
||||
}
|
||||
disabled={!isRecipientsTurn}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -374,14 +306,6 @@ export const DocumentSigningForm = ({
|
||||
onClose={() => !isAssistantSubmitting && setIsConfirmationDialogOpen(false)}
|
||||
onConfirm={handleAssistantConfirmDialogSubmit}
|
||||
isSubmitting={isAssistantSubmitting}
|
||||
allowDictateNextSigner={
|
||||
nextRecipient && document.documentMeta?.allowDictateNextSigner
|
||||
}
|
||||
defaultNextSigner={
|
||||
nextRecipient
|
||||
? { name: nextRecipient.name, email: nextRecipient.email }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
</>
|
||||
@@ -389,11 +313,7 @@ export const DocumentSigningForm = ({
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<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>
|
||||
|
||||
<hr className="border-border mb-8 mt-4" />
|
||||
@@ -417,75 +337,63 @@ export const DocumentSigningForm = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{hasSignatureField && (
|
||||
<div>
|
||||
<Label htmlFor="Signature">
|
||||
<Trans>Signature</Trans>
|
||||
</Label>
|
||||
<div>
|
||||
<Label htmlFor="Signature">
|
||||
<Trans>Signature</Trans>
|
||||
</Label>
|
||||
|
||||
<Card className="mt-2" gradient degrees={-120}>
|
||||
<CardContent className="p-0">
|
||||
<SignaturePad
|
||||
className="h-44 w-full"
|
||||
disabled={isSubmitting}
|
||||
defaultValue={signature ?? undefined}
|
||||
onValidityChange={(isValid) => {
|
||||
setSignatureValid(isValid);
|
||||
}}
|
||||
onChange={(value) => {
|
||||
if (signatureValid) {
|
||||
setSignature(value);
|
||||
}
|
||||
}}
|
||||
allowTypedSignature={document.documentMeta?.typedSignatureEnabled}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="mt-2" gradient degrees={-120}>
|
||||
<CardContent className="p-0">
|
||||
<SignaturePad
|
||||
className="h-44 w-full"
|
||||
disabled={isSubmitting}
|
||||
defaultValue={signature ?? undefined}
|
||||
onValidityChange={(isValid) => {
|
||||
setSignatureValid(isValid);
|
||||
}}
|
||||
onChange={(value) => {
|
||||
if (signatureValid) {
|
||||
setSignature(value);
|
||||
}
|
||||
}}
|
||||
allowTypedSignature={document.documentMeta?.typedSignatureEnabled}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{!signatureValid && (
|
||||
<div className="text-destructive mt-2 text-sm">
|
||||
<Trans>
|
||||
Signature is too small. Please provide a more complete signature.
|
||||
</Trans>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{hasSignatureField && !signatureValid && (
|
||||
<div className="text-destructive mt-2 text-sm">
|
||||
<Trans>
|
||||
Signature is too small. Please provide a more complete signature.
|
||||
</Trans>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<Button
|
||||
type="button"
|
||||
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||
onClick={async () => navigate(-1)}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<DocumentSigningCompleteDialog
|
||||
isSubmitting={isSubmitting}
|
||||
onSignatureComplete={handleSubmit(onFormSubmit)}
|
||||
documentTitle={document.title}
|
||||
fields={fields}
|
||||
fieldsValidated={fieldsValidated}
|
||||
role={recipient.role}
|
||||
disabled={!isRecipientsTurn}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div className="mt-6 flex flex-col gap-4 md:flex-row">
|
||||
<Button
|
||||
type="button"
|
||||
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||
onClick={async () => navigate(-1)}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<DocumentSigningCompleteDialog
|
||||
isSubmitting={isSubmitting || isAssistantSubmitting}
|
||||
documentTitle={document.title}
|
||||
fields={fields}
|
||||
fieldsValidated={fieldsValidated}
|
||||
disabled={!isRecipientsTurn}
|
||||
onSignatureComplete={async (nextSigner) => {
|
||||
await completeDocument(undefined, nextSigner);
|
||||
}}
|
||||
role={recipient.role}
|
||||
allowDictateNextSigner={
|
||||
nextRecipient && document.documentMeta?.allowDictateNextSigner
|
||||
}
|
||||
defaultNextSigner={
|
||||
nextRecipient
|
||||
? { name: nextRecipient.name, email: nextRecipient.email }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -40,9 +40,9 @@ import { DocumentReadOnlyFields } from '~/components/general/document/document-r
|
||||
|
||||
import { DocumentSigningRecipientProvider } from './document-signing-recipient-provider';
|
||||
|
||||
export type DocumentSigningPageViewProps = {
|
||||
recipient: RecipientWithFields;
|
||||
export type SigningPageViewProps = {
|
||||
document: DocumentAndSender;
|
||||
recipient: RecipientWithFields;
|
||||
fields: Field[];
|
||||
completedFields: CompletedField[];
|
||||
isRecipientsTurn: boolean;
|
||||
@@ -50,13 +50,13 @@ export type DocumentSigningPageViewProps = {
|
||||
};
|
||||
|
||||
export const DocumentSigningPageView = ({
|
||||
recipient,
|
||||
document,
|
||||
recipient,
|
||||
fields,
|
||||
completedFields,
|
||||
isRecipientsTurn,
|
||||
allRecipients = [],
|
||||
}: DocumentSigningPageViewProps) => {
|
||||
}: SigningPageViewProps) => {
|
||||
const { documentData, documentMeta } = document;
|
||||
|
||||
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(allRecipients?.[0]?.id);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { DocumentStatus } from '@prisma/client';
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
import { DownloadIcon } from 'lucide-react';
|
||||
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@@ -77,7 +76,7 @@ export const DocumentCertificateDownloadButton = ({
|
||||
className={cn('w-full sm:w-auto', className)}
|
||||
loading={isPending}
|
||||
variant="outline"
|
||||
disabled={!isDocumentCompleted(documentStatus)}
|
||||
disabled={documentStatus !== DocumentStatus.COMPLETED}
|
||||
onClick={() => void onDownloadCertificatesClick()}
|
||||
>
|
||||
{!isPending && <DownloadIcon className="mr-1.5 h-4 w-4" />}
|
||||
|
||||
@@ -71,7 +71,7 @@ export const DocumentEditForm = ({
|
||||
|
||||
const { recipients, fields } = document;
|
||||
|
||||
const { mutateAsync: updateDocumentSettings } = trpc.document.setSettingsForDocument.useMutation({
|
||||
const { mutateAsync: updateDocument } = trpc.document.setSettingsForDocument.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newData) => {
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
@@ -176,7 +176,7 @@ export const DocumentEditForm = ({
|
||||
try {
|
||||
const { timezone, dateFormat, redirectUrl, language } = data.meta;
|
||||
|
||||
await updateDocumentSettings({
|
||||
await updateDocument({
|
||||
documentId: document.id,
|
||||
data: {
|
||||
title: data.title,
|
||||
@@ -213,13 +213,6 @@ export const DocumentEditForm = ({
|
||||
signingOrder: data.signingOrder,
|
||||
}),
|
||||
|
||||
updateDocumentSettings({
|
||||
documentId: document.id,
|
||||
meta: {
|
||||
allowDictateNextSigner: data.allowDictateNextSigner,
|
||||
},
|
||||
}),
|
||||
|
||||
setRecipients({
|
||||
documentId: document.id,
|
||||
recipients: data.signers.map((signer) => ({
|
||||
@@ -249,7 +242,7 @@ export const DocumentEditForm = ({
|
||||
fields: data.fields,
|
||||
});
|
||||
|
||||
await updateDocumentSettings({
|
||||
await updateDocument({
|
||||
documentId: document.id,
|
||||
|
||||
meta: {
|
||||
@@ -372,7 +365,6 @@ export const DocumentEditForm = ({
|
||||
documentFlow={documentFlow.signers}
|
||||
recipients={recipients}
|
||||
signingOrder={document.documentMeta?.signingOrder}
|
||||
allowDictateNextSigner={document.documentMeta?.allowDictateNextSigner}
|
||||
fields={fields}
|
||||
isDocumentEnterprise={isDocumentEnterprise}
|
||||
onSubmit={onAddSignersFormSubmit}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { match } from 'ts-pattern';
|
||||
|
||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@@ -33,7 +32,7 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
|
||||
|
||||
const isRecipient = !!recipient;
|
||||
const isPending = document.status === DocumentStatus.PENDING;
|
||||
const isComplete = isDocumentCompleted(document);
|
||||
const isComplete = document.status === DocumentStatus.COMPLETED;
|
||||
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||
const role = recipient?.role;
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import { useNavigate } from 'react-router';
|
||||
|
||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||
@@ -64,7 +63,7 @@ export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownP
|
||||
const isDraft = document.status === DocumentStatus.DRAFT;
|
||||
const isPending = document.status === DocumentStatus.PENDING;
|
||||
const isDeleted = document.deletedAt !== null;
|
||||
const isComplete = isDocumentCompleted(document);
|
||||
const isComplete = document.status === DocumentStatus.COMPLETED;
|
||||
const isCurrentTeamDocument = team && document.team?.url === team.url;
|
||||
const canManageDocument = Boolean(isOwner || isCurrentTeamDocument);
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import { Link } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { formatSigningLink } from '@documenso/lib/utils/recipients';
|
||||
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
|
||||
import { SignatureIcon } from '@documenso/ui/icons/signature';
|
||||
@@ -49,7 +48,7 @@ export const DocumentPageViewRecipients = ({
|
||||
<Trans>Recipients</Trans>
|
||||
</h1>
|
||||
|
||||
{!isDocumentCompleted(document.status) && (
|
||||
{document.status !== DocumentStatus.COMPLETED && (
|
||||
<Link
|
||||
to={`${documentRootPath}/${document.id}/edit?step=signers`}
|
||||
title={_(msg`Modify recipients`)}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { HTMLAttributes } from 'react';
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { CheckCircle2, Clock, File, XCircle } from 'lucide-react';
|
||||
import { CheckCircle2, Clock, File } from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
|
||||
|
||||
import type { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
@@ -36,12 +36,6 @@ export const FRIENDLY_STATUS_MAP: Record<ExtendedDocumentStatus, FriendlyStatus>
|
||||
icon: File,
|
||||
color: 'text-yellow-500 dark:text-yellow-200',
|
||||
},
|
||||
REJECTED: {
|
||||
label: msg`Rejected`,
|
||||
labelExtended: msg`Document rejected`,
|
||||
icon: XCircle,
|
||||
color: 'text-red-500 dark:text-red-300',
|
||||
},
|
||||
INBOX: {
|
||||
label: msg`Inbox`,
|
||||
labelExtended: msg`Document inbox`,
|
||||
|
||||
@@ -2,9 +2,6 @@ import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { useRevalidator } from 'react-router';
|
||||
|
||||
/**
|
||||
* Not really used anymore, this causes random 500s when the user refreshes while this occurs.
|
||||
*/
|
||||
export const RefreshOnFocus = () => {
|
||||
const { revalidate, state } = useRevalidator();
|
||||
|
||||
|
||||
@@ -161,7 +161,6 @@ export const TemplateEditForm = ({
|
||||
templateId: template.id,
|
||||
meta: {
|
||||
signingOrder: data.signingOrder,
|
||||
allowDictateNextSigner: data.allowDictateNextSigner,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -272,7 +271,6 @@ export const TemplateEditForm = ({
|
||||
recipients={recipients}
|
||||
fields={fields}
|
||||
signingOrder={template.templateMeta?.signingOrder}
|
||||
allowDictateNextSigner={template.templateMeta?.allowDictateNextSigner}
|
||||
templateDirectLink={template.directLink}
|
||||
onSubmit={onAddTemplatePlaceholderFormSubmit}
|
||||
isEnterprise={isEnterprise}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { match } from 'ts-pattern';
|
||||
|
||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@@ -38,7 +37,7 @@ export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonPr
|
||||
const isRecipient = !!recipient;
|
||||
const isDraft = row.status === DocumentStatus.DRAFT;
|
||||
const isPending = row.status === DocumentStatus.PENDING;
|
||||
const isComplete = isDocumentCompleted(row.status);
|
||||
const isComplete = row.status === DocumentStatus.COMPLETED;
|
||||
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||
const role = recipient?.role;
|
||||
const isCurrentTeamDocument = team && row.team?.url === team.url;
|
||||
|
||||
@@ -22,7 +22,6 @@ import { Link } from 'react-router';
|
||||
|
||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||
@@ -67,7 +66,7 @@ export const DocumentsTableActionDropdown = ({ row }: DocumentsTableActionDropdo
|
||||
// const isRecipient = !!recipient;
|
||||
const isDraft = row.status === DocumentStatus.DRAFT;
|
||||
const isPending = row.status === DocumentStatus.PENDING;
|
||||
const isComplete = isDocumentCompleted(row.status);
|
||||
const isComplete = row.status === DocumentStatus.COMPLETED;
|
||||
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||
const isCurrentTeamDocument = team && row.team?.url === team.url;
|
||||
const canManageDocument = Boolean(isOwner || isCurrentTeamDocument);
|
||||
|
||||
@@ -9,8 +9,8 @@ import { match } from 'ts-pattern';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
import type { TFindDocumentsResponse } from '@documenso/trpc/server/document-router/schema';
|
||||
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||
@@ -77,7 +77,7 @@ export const DocumentsTable = ({ data, isLoading, isLoadingError }: DocumentsTab
|
||||
{
|
||||
header: _(msg`Actions`),
|
||||
cell: ({ row }) =>
|
||||
(!row.original.deletedAt || isDocumentCompleted(row.original.status)) && (
|
||||
(!row.original.deletedAt || row.original.status === ExtendedDocumentStatus.COMPLETED) && (
|
||||
<div className="flex items-center gap-x-4">
|
||||
<DocumentsTableActionButton row={row.original} />
|
||||
<DocumentsTableActionDropdown row={row.original} />
|
||||
|
||||
@@ -27,6 +27,7 @@ import { TooltipProvider } from '@documenso/ui/primitives/tooltip';
|
||||
import type { Route } from './+types/root';
|
||||
import stylesheet from './app.css?url';
|
||||
import { GenericErrorLayout } from './components/general/generic-error-layout';
|
||||
import { RefreshOnFocus } from './components/general/refresh-on-focus';
|
||||
import { langCookie } from './storage/lang-cookie.server';
|
||||
import { themeSessionResolver } from './storage/theme-session.server';
|
||||
import { appMetaTags } from './utils/meta';
|
||||
@@ -158,6 +159,8 @@ export function LayoutContent({ children }: { children: React.ReactNode }) {
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
|
||||
<RefreshOnFocus />
|
||||
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `window.__ENV__ = ${JSON.stringify(publicEnv)}`,
|
||||
|
||||
@@ -103,9 +103,7 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
|
||||
variant="outline"
|
||||
loading={isResealDocumentLoading}
|
||||
disabled={document.recipients.some(
|
||||
(recipient) =>
|
||||
recipient.signingStatus !== SigningStatus.SIGNED &&
|
||||
recipient.signingStatus !== SigningStatus.REJECTED,
|
||||
(recipient) => recipient.signingStatus !== SigningStatus.SIGNED,
|
||||
)}
|
||||
onClick={() => resealDocument({ id: document.id })}
|
||||
>
|
||||
|
||||
@@ -220,9 +220,6 @@ export default function DocumentPage() {
|
||||
.with(DocumentStatus.COMPLETED, () => (
|
||||
<Trans>This document has been signed by all recipients</Trans>
|
||||
))
|
||||
.with(DocumentStatus.REJECTED, () => (
|
||||
<Trans>This document has been rejected by a recipient</Trans>
|
||||
))
|
||||
.with(DocumentStatus.DRAFT, () => (
|
||||
<Trans>This document is currently a draft and has not been sent</Trans>
|
||||
))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Plural, Trans } from '@lingui/react/macro';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import { DocumentStatus as InternalDocumentStatus, TeamMemberRole } from '@prisma/client';
|
||||
import { ChevronLeft, Users2 } from 'lucide-react';
|
||||
import { Link, redirect } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
@@ -9,7 +9,6 @@ import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-ent
|
||||
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
|
||||
import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
|
||||
import { DocumentEditForm } from '~/components/general/document/document-edit-form';
|
||||
@@ -72,7 +71,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
throw redirect(documentRootPath);
|
||||
}
|
||||
|
||||
if (isDocumentCompleted(document.status)) {
|
||||
if (document.status === InternalDocumentStatus.COMPLETED) {
|
||||
throw redirect(`${documentRootPath}/${documentId}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps)
|
||||
<Trans>Document</Trans>
|
||||
</Link>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col justify-between truncate sm:flex-row">
|
||||
<div>
|
||||
<h1
|
||||
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
|
||||
@@ -137,8 +137,7 @@ export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps)
|
||||
>
|
||||
{document.title}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="mt-1 flex flex-col justify-between sm:flex-row">
|
||||
|
||||
<div className="mt-2.5 flex items-center gap-x-6">
|
||||
<DocumentStatusComponent
|
||||
inheritColor
|
||||
@@ -146,15 +145,16 @@ export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps)
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 flex w-full flex-row sm:mt-0 sm:w-auto sm:self-end">
|
||||
<DocumentCertificateDownloadButton
|
||||
className="mr-2"
|
||||
documentId={document.id}
|
||||
documentStatus={document.status}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DocumentAuditLogDownloadButton documentId={document.id} />
|
||||
</div>
|
||||
<div className="mt-4 flex w-full flex-row sm:mt-0 sm:w-auto sm:self-end">
|
||||
<DocumentCertificateDownloadButton
|
||||
className="mr-2"
|
||||
documentId={document.id}
|
||||
documentStatus={document.status}
|
||||
/>
|
||||
|
||||
<DocumentAuditLogDownloadButton documentId={document.id} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -163,7 +163,7 @@ export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps)
|
||||
{documentInformation.map((info, i) => (
|
||||
<div className="text-foreground text-sm" key={i}>
|
||||
<h3 className="font-semibold">{_(info.description)}</h3>
|
||||
<p className="text-muted-foreground truncate">{info.value}</p>
|
||||
<p className="text-muted-foreground">{info.value}</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ export default function DocumentsPage() {
|
||||
[ExtendedDocumentStatus.DRAFT]: 0,
|
||||
[ExtendedDocumentStatus.PENDING]: 0,
|
||||
[ExtendedDocumentStatus.COMPLETED]: 0,
|
||||
[ExtendedDocumentStatus.REJECTED]: 0,
|
||||
[ExtendedDocumentStatus.INBOX]: 0,
|
||||
[ExtendedDocumentStatus.ALL]: 0,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { FieldType, SigningStatus } from '@prisma/client';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import { DateTime } from 'luxon';
|
||||
import { redirect } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
@@ -159,13 +159,6 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
||||
log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED &&
|
||||
log.data.recipientId === recipientId,
|
||||
),
|
||||
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED]: auditLogs[
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED
|
||||
].filter(
|
||||
(log) =>
|
||||
log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED &&
|
||||
log.data.recipientId === recipientId,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -289,42 +282,25 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{logs.DOCUMENT_RECIPIENT_REJECTED[0] ? (
|
||||
<p className="text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Rejected`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{logs.DOCUMENT_RECIPIENT_REJECTED[0]
|
||||
? DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_REJECTED[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: _(msg`Unknown`)}
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Signed`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]
|
||||
? DateTime.fromJSDate(
|
||||
logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt,
|
||||
)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: _(msg`Unknown`)}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
<p className="text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Signed`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]
|
||||
? DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: _(msg`Unknown`)}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Reason`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{recipient.signingStatus === SigningStatus.REJECTED
|
||||
? recipient.rejectionReason
|
||||
: _(
|
||||
isOwner(recipient.email)
|
||||
? FRIENDLY_SIGNING_REASONS['__OWNER__']
|
||||
: FRIENDLY_SIGNING_REASONS[recipient.role],
|
||||
)}
|
||||
{_(
|
||||
isOwner(recipient.email)
|
||||
? FRIENDLY_SIGNING_REASONS['__OWNER__']
|
||||
: FRIENDLY_SIGNING_REASONS[recipient.role],
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { msg } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { PlusIcon } from 'lucide-react';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { Link, Outlet, isRouteErrorResponse } from 'react-router';
|
||||
import { Link, Outlet } from 'react-router';
|
||||
|
||||
import LogoIcon from '@documenso/assets/logo_icon.png';
|
||||
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
||||
@@ -16,8 +16,6 @@ import { BrandingLogo } from '~/components/general/branding-logo';
|
||||
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
|
||||
import { appMetaTags } from '~/utils/meta';
|
||||
|
||||
import type { Route } from './+types/_layout';
|
||||
|
||||
export function meta() {
|
||||
return appMetaTags('Profile');
|
||||
}
|
||||
@@ -98,9 +96,7 @@ export default function PublicProfileLayout() {
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
const errorCode = isRouteErrorResponse(error) ? error.status : 500;
|
||||
|
||||
export function ErrorBoundary() {
|
||||
const errorCodeMap = {
|
||||
404: {
|
||||
subHeading: msg`404 Profile not found`,
|
||||
@@ -111,7 +107,6 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
|
||||
return (
|
||||
<GenericErrorLayout
|
||||
errorCode={errorCode}
|
||||
errorCodeMap={errorCodeMap}
|
||||
secondaryButton={null}
|
||||
primaryButton={
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { Link, Outlet, isRouteErrorResponse } from 'react-router';
|
||||
import { Link, Outlet } from 'react-router';
|
||||
|
||||
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@@ -8,8 +8,6 @@ import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Header as AuthenticatedHeader } from '~/components/general/app-header';
|
||||
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
|
||||
* where we do not care whether they are authenticated or not.
|
||||
@@ -32,12 +30,9 @@ export default function RecipientLayout() {
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
const errorCode = isRouteErrorResponse(error) ? error.status : 500;
|
||||
|
||||
export function ErrorBoundary() {
|
||||
return (
|
||||
<GenericErrorLayout
|
||||
errorCode={errorCode}
|
||||
secondaryButton={null}
|
||||
primaryButton={
|
||||
<Button asChild className="w-32">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DocumentSigningOrder, DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import { Clock8 } from 'lucide-react';
|
||||
import { Link, redirect } from 'react-router';
|
||||
import { getOptionalLoaderContext } from 'server/utils/get-loader-session';
|
||||
@@ -13,7 +13,6 @@ import { viewedDocument } from '@documenso/lib/server-only/document/viewed-docum
|
||||
import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get-completed-fields-for-token';
|
||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||
import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn';
|
||||
import { getNextPendingRecipient } from '@documenso/lib/server-only/recipient/get-next-pending-recipient';
|
||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||
import { getRecipientsForAssistant } from '@documenso/lib/server-only/recipient/get-recipients-for-assistant';
|
||||
@@ -73,24 +72,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
? await getRecipientsForAssistant({
|
||||
token,
|
||||
})
|
||||
: [recipient];
|
||||
|
||||
if (
|
||||
document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL &&
|
||||
recipient.role !== RecipientRole.ASSISTANT
|
||||
) {
|
||||
const nextPendingRecipient = await getNextPendingRecipient({
|
||||
documentId: document.id,
|
||||
currentRecipientId: recipient.id,
|
||||
});
|
||||
|
||||
if (nextPendingRecipient) {
|
||||
allRecipients.push({
|
||||
...nextPendingRecipient,
|
||||
fields: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
: [];
|
||||
|
||||
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
@@ -178,7 +160,7 @@ export default function SigningPage() {
|
||||
recipientWithFields,
|
||||
} = data;
|
||||
|
||||
if (document.deletedAt || document.status === DocumentStatus.REJECTED) {
|
||||
if (document.deletedAt) {
|
||||
return (
|
||||
<div className="-mx-4 flex max-w-[100vw] flex-col items-center overflow-x-hidden px-4 pt-16 md:-mx-8 md:px-8 lg:pt-16 xl:pt-24">
|
||||
<SigningCard3D
|
||||
|
||||
@@ -17,7 +17,6 @@ import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-f
|
||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { env } from '@documenso/lib/utils/env';
|
||||
import DocumentDialog from '@documenso/ui/components/document/document-dialog';
|
||||
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
||||
@@ -206,12 +205,12 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
|
||||
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
|
||||
<DocumentShareButton documentId={document.id} token={recipient.token} />
|
||||
|
||||
{isDocumentCompleted(document.status) ? (
|
||||
{document.status === DocumentStatus.COMPLETED ? (
|
||||
<DocumentDownloadButton
|
||||
className="flex-1"
|
||||
fileName={document.title}
|
||||
documentData={document.documentData}
|
||||
disabled={!isDocumentCompleted(document.status)}
|
||||
disabled={document.status !== DocumentStatus.COMPLETED}
|
||||
/>
|
||||
) : (
|
||||
<DocumentDialog
|
||||
@@ -269,7 +268,7 @@ export const PollUntilDocumentCompleted = ({ document }: PollUntilDocumentComple
|
||||
const { revalidate } = useRevalidator();
|
||||
|
||||
useEffect(() => {
|
||||
if (isDocumentCompleted(document.status)) {
|
||||
if (document.status === DocumentStatus.COMPLETED) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { DocumentStatus, RecipientRole } from '@prisma/client';
|
||||
import { data } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@@ -14,7 +14,6 @@ import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-re
|
||||
import { getRecipientsForAssistant } from '@documenso/lib/server-only/recipient/get-recipients-for-assistant';
|
||||
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
|
||||
import { EmbedSignDocumentClientPage } from '~/components/embed/embed-document-signing-page';
|
||||
@@ -169,7 +168,7 @@ export default function EmbedSignDocumentPage() {
|
||||
recipient={recipient}
|
||||
fields={fields}
|
||||
metadata={document.documentMeta}
|
||||
isCompleted={isDocumentCompleted(document.status)}
|
||||
isCompleted={document.status === DocumentStatus.COMPLETED}
|
||||
hidePoweredBy={
|
||||
isCommunityPlan || isPlatformDocument || isEnterpriseDocument || hidePoweredBy
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ const themeSessionStorage = createCookieSessionStorage({
|
||||
secrets: ['insecure-secret-do-not-care'],
|
||||
secure: useSecureCookies,
|
||||
domain: getCookieDomain(),
|
||||
maxAge: 60 * 60 * 24 * 365,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -99,6 +99,5 @@
|
||||
"vite": "^6.1.0",
|
||||
"vite-plugin-babel-macros": "^1.0.6",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"version": "1.10.0-rc.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ command -v docker >/dev/null 2>&1 || {
|
||||
SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
|
||||
MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")"
|
||||
|
||||
# Get Git information
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
@@ -16,39 +15,12 @@ echo "Building docker image for monorepo at $MONOREPO_ROOT"
|
||||
echo "App version: $APP_VERSION"
|
||||
echo "Git SHA: $GIT_SHA"
|
||||
|
||||
# Build with temporary base tag
|
||||
docker build -f "$SCRIPT_DIR/Dockerfile" \
|
||||
--progress=plain \
|
||||
-t "documenso-base" \
|
||||
-t "documenso/documenso:latest" \
|
||||
-t "documenso/documenso:$GIT_SHA" \
|
||||
-t "documenso/documenso:$APP_VERSION" \
|
||||
-t "ghcr.io/documenso/documenso:latest" \
|
||||
-t "ghcr.io/documenso/documenso:$GIT_SHA" \
|
||||
-t "ghcr.io/documenso/documenso:$APP_VERSION" \
|
||||
"$MONOREPO_ROOT"
|
||||
|
||||
# Handle repository tagging
|
||||
if [ ! -z "$DOCKER_REPOSITORY" ]; then
|
||||
echo "Using custom repository: $DOCKER_REPOSITORY"
|
||||
|
||||
# Add tags for custom repository
|
||||
docker tag "documenso-base" "$DOCKER_REPOSITORY:latest"
|
||||
docker tag "documenso-base" "$DOCKER_REPOSITORY:$GIT_SHA"
|
||||
|
||||
# Add version tag if available
|
||||
if [ ! -z "$APP_VERSION" ] && [ "$APP_VERSION" != "undefined" ]; then
|
||||
docker tag "documenso-base" "$DOCKER_REPOSITORY:$APP_VERSION"
|
||||
fi
|
||||
else
|
||||
echo "Using default repositories: dockerhub and ghcr.io"
|
||||
|
||||
# Add tags for both default repositories
|
||||
docker tag "documenso-base" "documenso/documenso:latest"
|
||||
docker tag "documenso-base" "documenso/documenso:$GIT_SHA"
|
||||
docker tag "documenso-base" "ghcr.io/documenso/documenso:latest"
|
||||
docker tag "documenso-base" "ghcr.io/documenso/documenso:$GIT_SHA"
|
||||
|
||||
# Add version tags if available
|
||||
if [ ! -z "$APP_VERSION" ] && [ "$APP_VERSION" != "undefined" ]; then
|
||||
docker tag "documenso-base" "documenso/documenso:$APP_VERSION"
|
||||
docker tag "documenso-base" "ghcr.io/documenso/documenso:$APP_VERSION"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove the temporary base tag
|
||||
docker rmi "documenso-base"
|
||||
|
||||
@@ -9,11 +9,11 @@ SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
|
||||
MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")"
|
||||
|
||||
# Get the platform from environment variable or set to linux/amd64 if not set
|
||||
# quote the string to prevent word splitting
|
||||
if [ -z "$PLATFORM" ]; then
|
||||
PLATFORM="linux/amd64"
|
||||
fi
|
||||
|
||||
# Get Git information
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
@@ -21,41 +21,14 @@ echo "Building docker image for monorepo at $MONOREPO_ROOT"
|
||||
echo "App version: $APP_VERSION"
|
||||
echo "Git SHA: $GIT_SHA"
|
||||
|
||||
# Build with temporary base tag
|
||||
docker buildx build \
|
||||
-f "$SCRIPT_DIR/Dockerfile" \
|
||||
--platform=$PLATFORM \
|
||||
--progress=plain \
|
||||
-t "documenso-base" \
|
||||
-t "documenso/documenso:latest" \
|
||||
-t "documenso/documenso:$GIT_SHA" \
|
||||
-t "documenso/documenso:$APP_VERSION" \
|
||||
-t "ghcr.io/documenso/documenso:latest" \
|
||||
-t "ghcr.io/documenso/documenso:$GIT_SHA" \
|
||||
-t "ghcr.io/documenso/documenso:$APP_VERSION" \
|
||||
"$MONOREPO_ROOT"
|
||||
|
||||
# Handle repository tagging
|
||||
if [ ! -z "$DOCKER_REPOSITORY" ]; then
|
||||
echo "Using custom repository: $DOCKER_REPOSITORY"
|
||||
|
||||
# Add tags for custom repository
|
||||
docker tag "documenso-base" "$DOCKER_REPOSITORY:latest"
|
||||
docker tag "documenso-base" "$DOCKER_REPOSITORY:$GIT_SHA"
|
||||
|
||||
# Add version tag if available
|
||||
if [ ! -z "$APP_VERSION" ] && [ "$APP_VERSION" != "undefined" ]; then
|
||||
docker tag "documenso-base" "$DOCKER_REPOSITORY:$APP_VERSION"
|
||||
fi
|
||||
else
|
||||
echo "Using default repositories: dockerhub and ghcr.io"
|
||||
|
||||
# Add tags for both default repositories
|
||||
docker tag "documenso-base" "documenso/documenso:latest"
|
||||
docker tag "documenso-base" "documenso/documenso:$GIT_SHA"
|
||||
docker tag "documenso-base" "ghcr.io/documenso/documenso:latest"
|
||||
docker tag "documenso-base" "ghcr.io/documenso/documenso:$GIT_SHA"
|
||||
|
||||
# Add version tags if available
|
||||
if [ ! -z "$APP_VERSION" ] && [ "$APP_VERSION" != "undefined" ]; then
|
||||
docker tag "documenso-base" "documenso/documenso:$APP_VERSION"
|
||||
docker tag "documenso-base" "ghcr.io/documenso/documenso:$APP_VERSION"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove the temporary base tag
|
||||
docker rmi "documenso-base"
|
||||
|
||||
5
package-lock.json
generated
5
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@documenso/root",
|
||||
"version": "1.10.0-rc.0",
|
||||
"version": "1.9.0-rc.11",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@documenso/root",
|
||||
"version": "1.10.0-rc.0",
|
||||
"version": "1.9.0-rc.11",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
@@ -95,7 +95,6 @@
|
||||
},
|
||||
"apps/remix": {
|
||||
"name": "@documenso/remix",
|
||||
"version": "1.10.0-rc.0",
|
||||
"dependencies": {
|
||||
"@documenso/api": "*",
|
||||
"@documenso/assets": "*",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "1.10.0-rc.0",
|
||||
"version": "1.9.0-rc.11",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev --filter=@documenso/remix",
|
||||
@@ -14,7 +14,7 @@
|
||||
"prepare": "husky && husky install || true",
|
||||
"commitlint": "commitlint --edit",
|
||||
"clean": "turbo run clean && rimraf node_modules",
|
||||
"d": "npm run dx && npm run translate:compile && npm run dev",
|
||||
"d": "npm run dx && npm run dev",
|
||||
"dx": "npm i && npm run dx:up && npm run prisma:migrate-dev && npm run prisma:seed",
|
||||
"dx:up": "docker compose -f docker/development/compose.yml up -d",
|
||||
"dx:down": "docker compose -f docker/development/compose.yml down",
|
||||
@@ -80,4 +80,4 @@
|
||||
"trigger.dev": {
|
||||
"endpointId": "documenso-app"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { DocumentDataType, SigningStatus, TeamMemberRole } from '@prisma/client';
|
||||
import { DocumentDataType, DocumentStatus, SigningStatus, TeamMemberRole } from '@prisma/client';
|
||||
import { tsr } from '@ts-rest/serverless/fetch';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@@ -50,7 +50,6 @@ import {
|
||||
getPresignGetUrl,
|
||||
getPresignPostUrl,
|
||||
} from '@documenso/lib/universal/upload/server-actions';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@@ -177,7 +176,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
||||
};
|
||||
}
|
||||
|
||||
if (!isDocumentCompleted(document.status)) {
|
||||
if (document.status !== DocumentStatus.COMPLETED) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
@@ -323,7 +322,6 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
||||
dateFormat: dateFormat?.value,
|
||||
redirectUrl: body.meta.redirectUrl,
|
||||
signingOrder: body.meta.signingOrder,
|
||||
allowDictateNextSigner: body.meta.allowDictateNextSigner,
|
||||
language: body.meta.language,
|
||||
typedSignatureEnabled: body.meta.typedSignatureEnabled,
|
||||
distributionMethod: body.meta.distributionMethod,
|
||||
@@ -582,7 +580,6 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
recipients: body.recipients,
|
||||
prefillFields: body.prefillFields,
|
||||
override: {
|
||||
title: body.title,
|
||||
...body.meta,
|
||||
@@ -671,7 +668,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
||||
};
|
||||
}
|
||||
|
||||
if (isDocumentCompleted(document.status)) {
|
||||
if (document.status === DocumentStatus.COMPLETED) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
@@ -774,7 +771,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
||||
};
|
||||
}
|
||||
|
||||
if (isDocumentCompleted(document.status)) {
|
||||
if (document.status === DocumentStatus.COMPLETED) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
@@ -865,7 +862,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
||||
};
|
||||
}
|
||||
|
||||
if (isDocumentCompleted(document.status)) {
|
||||
if (document.status === DocumentStatus.COMPLETED) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
@@ -924,7 +921,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
||||
};
|
||||
}
|
||||
|
||||
if (isDocumentCompleted(document.status)) {
|
||||
if (document.status === DocumentStatus.COMPLETED) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
@@ -989,7 +986,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
||||
};
|
||||
}
|
||||
|
||||
if (isDocumentCompleted(document.status)) {
|
||||
if (document.status === DocumentStatus.COMPLETED) {
|
||||
return {
|
||||
status: 400,
|
||||
body: { message: 'Document is already completed' },
|
||||
@@ -1151,7 +1148,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
||||
};
|
||||
}
|
||||
|
||||
if (isDocumentCompleted(document.status)) {
|
||||
if (document.status === DocumentStatus.COMPLETED) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
@@ -1239,7 +1236,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
||||
};
|
||||
}
|
||||
|
||||
if (isDocumentCompleted(document.status)) {
|
||||
if (document.status === DocumentStatus.COMPLETED) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
ZRecipientActionAuthTypesSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZFieldMetaPrefillFieldsSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
@@ -96,7 +96,7 @@ export const ZSendDocumentForSigningMutationSchema = z
|
||||
'Whether to send completion emails when the document is fully signed. This will override the document email settings.',
|
||||
}),
|
||||
})
|
||||
.or(z.any().transform(() => ({ sendEmail: true, sendCompletionEmails: undefined })));
|
||||
.or(z.literal('').transform(() => ({ sendEmail: true, sendCompletionEmails: undefined })));
|
||||
|
||||
export type TSendDocumentForSigningMutationSchema = typeof ZSendDocumentForSigningMutationSchema;
|
||||
|
||||
@@ -155,7 +155,6 @@ export const ZCreateDocumentMutationSchema = z.object({
|
||||
}),
|
||||
redirectUrl: z.string(),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||
allowDictateNextSigner: z.boolean().optional(),
|
||||
language: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
|
||||
typedSignatureEnabled: z.boolean().optional().default(true),
|
||||
distributionMethod: z.nativeEnum(DocumentDistributionMethod).optional(),
|
||||
@@ -219,7 +218,6 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
|
||||
dateFormat: z.string(),
|
||||
redirectUrl: z.string(),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||
allowDictateNextSigner: z.boolean().optional(),
|
||||
language: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
|
||||
})
|
||||
.partial()
|
||||
@@ -287,7 +285,6 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
|
||||
dateFormat: z.string(),
|
||||
redirectUrl: ZUrlSchema,
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||
allowDictateNextSigner: z.boolean(),
|
||||
language: z.enum(SUPPORTED_LANGUAGE_CODES),
|
||||
distributionMethod: z.nativeEnum(DocumentDistributionMethod),
|
||||
typedSignatureEnabled: z.boolean(),
|
||||
@@ -302,7 +299,6 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
|
||||
})
|
||||
.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<
|
||||
|
||||
@@ -1,614 +0,0 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -1,602 +0,0 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -210,7 +210,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient au
|
||||
}),
|
||||
},
|
||||
],
|
||||
fields: [FieldType.DATE, FieldType.SIGNATURE],
|
||||
fields: [FieldType.DATE],
|
||||
});
|
||||
|
||||
for (const recipient of recipients) {
|
||||
@@ -307,7 +307,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient an
|
||||
}),
|
||||
},
|
||||
],
|
||||
fields: [FieldType.DATE, FieldType.SIGNATURE],
|
||||
fields: [FieldType.DATE],
|
||||
updateDocumentOptions: {
|
||||
authOptions: createDocumentAuthOptions({
|
||||
globalAccessAuth: null,
|
||||
|
||||
@@ -1,390 +0,0 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
DocumentStatus,
|
||||
FieldType,
|
||||
RecipientRole,
|
||||
SigningStatus,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { signSignaturePad } from '../fixtures/signature';
|
||||
|
||||
test('[NEXT_RECIPIENT_DICTATION]: should allow updating next recipient when dictation is enabled', async ({
|
||||
page,
|
||||
}) => {
|
||||
const user = await seedUser();
|
||||
const firstSigner = await seedUser();
|
||||
const secondSigner = await seedUser();
|
||||
const thirdSigner = await seedUser();
|
||||
|
||||
const { recipients, document } = await seedPendingDocumentWithFullFields({
|
||||
owner: user,
|
||||
recipients: [firstSigner, secondSigner, thirdSigner],
|
||||
recipientsCreateOptions: [{ signingOrder: 1 }, { signingOrder: 2 }, { signingOrder: 3 }],
|
||||
updateDocumentOptions: {
|
||||
documentMeta: {
|
||||
upsert: {
|
||||
create: {
|
||||
allowDictateNextSigner: true,
|
||||
signingOrder: DocumentSigningOrder.SEQUENTIAL,
|
||||
},
|
||||
update: {
|
||||
allowDictateNextSigner: true,
|
||||
signingOrder: DocumentSigningOrder.SEQUENTIAL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const firstRecipient = recipients[0];
|
||||
const { token, fields } = firstRecipient;
|
||||
|
||||
const signUrl = `/sign/${token}`;
|
||||
|
||||
await page.goto(signUrl);
|
||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||
|
||||
await signSignaturePad(page);
|
||||
|
||||
// Fill in all fields
|
||||
for (const field of fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
|
||||
if (field.type === FieldType.TEXT) {
|
||||
await page.locator('#custom-text').fill('TEXT');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
}
|
||||
|
||||
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||
}
|
||||
|
||||
// Complete signing and update next recipient
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
|
||||
// Verify next recipient info is shown
|
||||
await expect(page.getByRole('dialog')).toBeVisible();
|
||||
await expect(page.getByText('The next recipient to sign this document will be')).toBeVisible();
|
||||
|
||||
// Update next recipient
|
||||
await page.locator('button').filter({ hasText: 'Update Recipient' }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Use dialog context to ensure we're targeting the correct form fields
|
||||
const dialog = page.getByRole('dialog');
|
||||
await dialog.getByLabel('Name').fill('New Recipient');
|
||||
await dialog.getByLabel('Email').fill('new.recipient@example.com');
|
||||
|
||||
// Submit and verify completion
|
||||
await page.getByRole('button', { name: 'Sign' }).click();
|
||||
await page.waitForURL(`${signUrl}/complete`);
|
||||
|
||||
// Verify document and recipient states
|
||||
const updatedDocument = await prisma.document.findUniqueOrThrow({
|
||||
where: { id: document.id },
|
||||
include: {
|
||||
recipients: {
|
||||
orderBy: { signingOrder: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Document should still be pending as there are more recipients
|
||||
expect(updatedDocument.status).toBe(DocumentStatus.PENDING);
|
||||
|
||||
// First recipient should be completed
|
||||
const updatedFirstRecipient = updatedDocument.recipients[0];
|
||||
expect(updatedFirstRecipient.signingStatus).toBe(SigningStatus.SIGNED);
|
||||
|
||||
// Second recipient should be the new recipient
|
||||
const updatedSecondRecipient = updatedDocument.recipients[1];
|
||||
expect(updatedSecondRecipient.name).toBe('New Recipient');
|
||||
expect(updatedSecondRecipient.email).toBe('new.recipient@example.com');
|
||||
expect(updatedSecondRecipient.signingOrder).toBe(2);
|
||||
expect(updatedSecondRecipient.signingStatus).toBe(SigningStatus.NOT_SIGNED);
|
||||
});
|
||||
|
||||
test('[NEXT_RECIPIENT_DICTATION]: should not show dictation UI when disabled', async ({ page }) => {
|
||||
const user = await seedUser();
|
||||
const firstSigner = await seedUser();
|
||||
const secondSigner = await seedUser();
|
||||
|
||||
const { recipients, document } = await seedPendingDocumentWithFullFields({
|
||||
owner: user,
|
||||
recipients: [firstSigner, secondSigner],
|
||||
recipientsCreateOptions: [{ signingOrder: 1 }, { signingOrder: 2 }],
|
||||
updateDocumentOptions: {
|
||||
documentMeta: {
|
||||
upsert: {
|
||||
create: {
|
||||
allowDictateNextSigner: false,
|
||||
signingOrder: DocumentSigningOrder.SEQUENTIAL,
|
||||
},
|
||||
update: {
|
||||
allowDictateNextSigner: false,
|
||||
signingOrder: DocumentSigningOrder.SEQUENTIAL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const firstRecipient = recipients[0];
|
||||
const { token, fields } = firstRecipient;
|
||||
|
||||
const signUrl = `/sign/${token}`;
|
||||
|
||||
await page.goto(signUrl);
|
||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||
|
||||
await signSignaturePad(page);
|
||||
|
||||
// Fill in all fields
|
||||
for (const field of fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
|
||||
if (field.type === FieldType.TEXT) {
|
||||
await page.locator('#custom-text').fill('TEXT');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
}
|
||||
|
||||
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||
}
|
||||
|
||||
// Complete signing
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
|
||||
// Verify next recipient UI is not shown
|
||||
await expect(
|
||||
page.getByText('The next recipient to sign this document will be'),
|
||||
).not.toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Update Recipient' })).not.toBeVisible();
|
||||
|
||||
// Submit and verify completion
|
||||
await page.getByRole('button', { name: 'Sign' }).click();
|
||||
await page.waitForURL(`${signUrl}/complete`);
|
||||
|
||||
// Verify document and recipient states
|
||||
|
||||
const updatedDocument = await prisma.document.findUniqueOrThrow({
|
||||
where: { id: document.id },
|
||||
include: {
|
||||
recipients: {
|
||||
orderBy: { signingOrder: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Document should still be pending as there are more recipients
|
||||
expect(updatedDocument.status).toBe(DocumentStatus.PENDING);
|
||||
|
||||
// First recipient should be completed
|
||||
const updatedFirstRecipient = updatedDocument.recipients[0];
|
||||
expect(updatedFirstRecipient.signingStatus).toBe(SigningStatus.SIGNED);
|
||||
|
||||
// Second recipient should remain unchanged
|
||||
const updatedSecondRecipient = updatedDocument.recipients[1];
|
||||
expect(updatedSecondRecipient.email).toBe(secondSigner.email);
|
||||
expect(updatedSecondRecipient.signingOrder).toBe(2);
|
||||
expect(updatedSecondRecipient.signingStatus).toBe(SigningStatus.NOT_SIGNED);
|
||||
});
|
||||
|
||||
test('[NEXT_RECIPIENT_DICTATION]: should work with parallel signing flow', async ({ page }) => {
|
||||
const user = await seedUser();
|
||||
const firstSigner = await seedUser();
|
||||
const secondSigner = await seedUser();
|
||||
|
||||
const { recipients, document } = await seedPendingDocumentWithFullFields({
|
||||
owner: user,
|
||||
recipients: [firstSigner, secondSigner],
|
||||
recipientsCreateOptions: [{ signingOrder: 1 }, { signingOrder: 2 }],
|
||||
updateDocumentOptions: {
|
||||
documentMeta: {
|
||||
upsert: {
|
||||
create: {
|
||||
allowDictateNextSigner: false,
|
||||
signingOrder: DocumentSigningOrder.PARALLEL,
|
||||
},
|
||||
update: {
|
||||
allowDictateNextSigner: false,
|
||||
signingOrder: DocumentSigningOrder.PARALLEL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Test both recipients can sign in parallel
|
||||
for (const recipient of recipients) {
|
||||
const { token, fields } = recipient;
|
||||
const signUrl = `/sign/${token}`;
|
||||
|
||||
await page.goto(signUrl);
|
||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||
|
||||
await signSignaturePad(page);
|
||||
|
||||
// Fill in all fields
|
||||
for (const field of fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
|
||||
if (field.type === FieldType.TEXT) {
|
||||
await page.locator('#custom-text').fill('TEXT');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
}
|
||||
|
||||
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||
}
|
||||
|
||||
// Complete signing
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
|
||||
// Verify next recipient UI is not shown in parallel flow
|
||||
await expect(
|
||||
page.getByText('The next recipient to sign this document will be'),
|
||||
).not.toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Update Recipient' })).not.toBeVisible();
|
||||
|
||||
// Submit and verify completion
|
||||
await page.getByRole('button', { name: 'Sign' }).click();
|
||||
await page.waitForURL(`${signUrl}/complete`);
|
||||
}
|
||||
|
||||
// Verify final document and recipient states
|
||||
await expect(async () => {
|
||||
const updatedDocument = await prisma.document.findUniqueOrThrow({
|
||||
where: { id: document.id },
|
||||
include: {
|
||||
recipients: {
|
||||
orderBy: { signingOrder: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Document should be completed since all recipients have signed
|
||||
expect(updatedDocument.status).toBe(DocumentStatus.COMPLETED);
|
||||
|
||||
// All recipients should be completed
|
||||
for (const recipient of updatedDocument.recipients) {
|
||||
expect(recipient.signingStatus).toBe(SigningStatus.SIGNED);
|
||||
}
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('[NEXT_RECIPIENT_DICTATION]: should allow assistant to dictate next signer', async ({
|
||||
page,
|
||||
}) => {
|
||||
const user = await seedUser();
|
||||
const assistant = await seedUser();
|
||||
const signer = await seedUser();
|
||||
const thirdSigner = await seedUser();
|
||||
|
||||
const { recipients, document } = await seedPendingDocumentWithFullFields({
|
||||
owner: user,
|
||||
recipients: [assistant, signer, thirdSigner],
|
||||
recipientsCreateOptions: [
|
||||
{ signingOrder: 1, role: RecipientRole.ASSISTANT },
|
||||
{ signingOrder: 2, role: RecipientRole.SIGNER },
|
||||
{ signingOrder: 3, role: RecipientRole.SIGNER },
|
||||
],
|
||||
updateDocumentOptions: {
|
||||
documentMeta: {
|
||||
upsert: {
|
||||
create: {
|
||||
allowDictateNextSigner: true,
|
||||
signingOrder: DocumentSigningOrder.SEQUENTIAL,
|
||||
},
|
||||
update: {
|
||||
allowDictateNextSigner: true,
|
||||
signingOrder: DocumentSigningOrder.SEQUENTIAL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const assistantRecipient = recipients[0];
|
||||
const { token, fields } = assistantRecipient;
|
||||
|
||||
const signUrl = `/sign/${token}`;
|
||||
|
||||
await page.goto(signUrl);
|
||||
await expect(page.getByRole('heading', { name: 'Assist Document' })).toBeVisible();
|
||||
|
||||
await page.getByRole('radio', { name: assistantRecipient.name }).click();
|
||||
|
||||
// Fill in all fields
|
||||
for (const field of fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
|
||||
if (field.type === FieldType.SIGNATURE) {
|
||||
await signSignaturePad(page);
|
||||
await page.getByRole('button', { name: 'Sign', exact: true }).click();
|
||||
}
|
||||
|
||||
if (field.type === FieldType.TEXT) {
|
||||
await page.locator('#custom-text').fill('TEXT');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
}
|
||||
|
||||
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||
}
|
||||
|
||||
// Complete assisting and update next recipient
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
// Verify next recipient info is shown
|
||||
await expect(page.getByRole('dialog')).toBeVisible();
|
||||
await expect(page.getByText('The next recipient to sign this document will be')).toBeVisible();
|
||||
|
||||
// Update next recipient
|
||||
await page.locator('button').filter({ hasText: 'Update Recipient' }).click();
|
||||
|
||||
// Use dialog context to ensure we're targeting the correct form fields
|
||||
const dialog = page.getByRole('dialog');
|
||||
await dialog.getByLabel('Name').fill('New Signer');
|
||||
await dialog.getByLabel('Email').fill('new.signer@example.com');
|
||||
|
||||
// Submit and verify completion
|
||||
await page.getByRole('button', { name: /Continue|Proceed/i }).click();
|
||||
await page.waitForURL(`${signUrl}/complete`);
|
||||
|
||||
// Verify document and recipient states
|
||||
await expect(async () => {
|
||||
const updatedDocument = await prisma.document.findUniqueOrThrow({
|
||||
where: { id: document.id },
|
||||
include: {
|
||||
recipients: {
|
||||
orderBy: { signingOrder: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Document should still be pending as there are more recipients
|
||||
expect(updatedDocument.status).toBe(DocumentStatus.PENDING);
|
||||
|
||||
// Assistant should be completed
|
||||
const updatedAssistant = updatedDocument.recipients[0];
|
||||
expect(updatedAssistant.signingStatus).toBe(SigningStatus.SIGNED);
|
||||
expect(updatedAssistant.role).toBe(RecipientRole.ASSISTANT);
|
||||
|
||||
// Second recipient should be the new signer
|
||||
const updatedSigner = updatedDocument.recipients[1];
|
||||
expect(updatedSigner.name).toBe('New Signer');
|
||||
expect(updatedSigner.email).toBe('new.signer@example.com');
|
||||
expect(updatedSigner.signingOrder).toBe(2);
|
||||
expect(updatedSigner.signingStatus).toBe(SigningStatus.NOT_SIGNED);
|
||||
expect(updatedSigner.role).toBe(RecipientRole.SIGNER);
|
||||
|
||||
// Third recipient should remain unchanged
|
||||
const thirdRecipient = updatedDocument.recipients[2];
|
||||
expect(thirdRecipient.email).toBe(thirdSigner.email);
|
||||
expect(thirdRecipient.signingOrder).toBe(3);
|
||||
expect(thirdRecipient.signingStatus).toBe(SigningStatus.NOT_SIGNED);
|
||||
expect(thirdRecipient.role).toBe(RecipientRole.SIGNER);
|
||||
}).toPass();
|
||||
});
|
||||
@@ -377,9 +377,7 @@ test('[DOCUMENT_FLOW]: should be able to approve a document', async ({ page }) =
|
||||
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||
}
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: role === RecipientRole.SIGNER ? 'Complete' : 'Approve' })
|
||||
.click();
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
await page
|
||||
.getByRole('button', { name: role === RecipientRole.SIGNER ? 'Sign' : 'Approve' })
|
||||
.click();
|
||||
@@ -449,7 +447,7 @@ test('[DOCUMENT_FLOW]: should be able to create, send with redirect url, sign a
|
||||
const { status } = await getDocumentByToken(token);
|
||||
expect(status).toBe(DocumentStatus.PENDING);
|
||||
|
||||
await page.getByRole('button', { name: 'Approve' }).click();
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
await expect(page.getByRole('dialog').getByText('Complete Approval').first()).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Approve' }).click();
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ test('[PUBLIC_PROFILE]: create profile', async ({ page }) => {
|
||||
// Go back to public profile page.
|
||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/settings/public-profile`);
|
||||
await page.getByRole('switch').click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Assert values.
|
||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/p/${publicProfileUrl}`);
|
||||
@@ -128,7 +127,6 @@ test('[PUBLIC_PROFILE]: create team profile', async ({ page }) => {
|
||||
// Go back to public profile page.
|
||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/settings/public-profile`);
|
||||
await page.getByRole('switch').click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Assert values.
|
||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/p/${publicProfileUrl}`);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getPlatformPlanPriceIds } from '../stripe/get-platform-plan-prices';
|
||||
|
||||
export type IsDocumentPlatformOptions = Pick<Document, 'id' | 'userId' | 'teamId'>;
|
||||
export type IsDocumentPlatformOptions = Pick<Document, 'userId' | 'teamId'>;
|
||||
|
||||
/**
|
||||
* Whether the user is platform, or has permission to use platform features on
|
||||
|
||||
@@ -8,14 +8,12 @@ export interface TemplateDocumentCancelProps {
|
||||
inviterEmail: string;
|
||||
documentName: string;
|
||||
assetBaseUrl: string;
|
||||
cancellationReason?: string;
|
||||
}
|
||||
|
||||
export const TemplateDocumentCancel = ({
|
||||
inviterName,
|
||||
documentName,
|
||||
assetBaseUrl,
|
||||
cancellationReason,
|
||||
}: TemplateDocumentCancelProps) => {
|
||||
return (
|
||||
<>
|
||||
@@ -36,12 +34,6 @@ export const TemplateDocumentCancel = ({
|
||||
<Text className="my-1 text-center text-base text-slate-400">
|
||||
<Trans>You don't need to sign it anymore.</Trans>
|
||||
</Text>
|
||||
|
||||
{cancellationReason && (
|
||||
<Text className="mt-4 text-center text-base">
|
||||
<Trans>Reason for cancellation: {cancellationReason}</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,6 @@ export const DocumentCancelTemplate = ({
|
||||
inviterEmail = 'lucas@documenso.com',
|
||||
documentName = 'Open Source Pledge.pdf',
|
||||
assetBaseUrl = 'http://localhost:3002',
|
||||
cancellationReason,
|
||||
}: DocumentCancelEmailTemplateProps) => {
|
||||
const { _ } = useLingui();
|
||||
const branding = useBranding();
|
||||
@@ -49,7 +48,6 @@ export const DocumentCancelTemplate = ({
|
||||
inviterEmail={inviterEmail}
|
||||
documentName={documentName}
|
||||
assetBaseUrl={assetBaseUrl}
|
||||
cancellationReason={cancellationReason}
|
||||
/>
|
||||
</Section>
|
||||
</Container>
|
||||
|
||||
@@ -8,9 +8,6 @@ export const DOCUMENT_STATUS: {
|
||||
[DocumentStatus.COMPLETED]: {
|
||||
description: msg`Completed`,
|
||||
},
|
||||
[DocumentStatus.REJECTED]: {
|
||||
description: msg`Rejected`,
|
||||
},
|
||||
[DocumentStatus.DRAFT]: {
|
||||
description: msg`Draft`,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { JobClient } from './client/client';
|
||||
import { SEND_CONFIRMATION_EMAIL_JOB_DEFINITION } from './definitions/emails/send-confirmation-email';
|
||||
import { SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-cancelled-emails';
|
||||
import { SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION } from './definitions/emails/send-password-reset-success-email';
|
||||
import { SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-recipient-signed-email';
|
||||
import { SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION } from './definitions/emails/send-rejection-emails';
|
||||
@@ -25,7 +24,6 @@ export const jobsClient = new JobClient([
|
||||
SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION,
|
||||
SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION,
|
||||
SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION,
|
||||
SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION,
|
||||
BULK_SEND_TEMPLATE_JOB_DEFINITION,
|
||||
] as const);
|
||||
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { ReadStatus, SendStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendDocumentCancelledEmailsJobDefinition } from './send-document-cancelled-emails';
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
io,
|
||||
}: {
|
||||
payload: TSendDocumentCancelledEmailsJobDefinition;
|
||||
io: JobRunIO;
|
||||
}) => {
|
||||
const { documentId, cancellationReason } = payload;
|
||||
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
team: {
|
||||
select: {
|
||||
teamEmail: true,
|
||||
name: true,
|
||||
url: true,
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { documentMeta, user: documentOwner } = document;
|
||||
|
||||
// Check if document cancellation emails are enabled
|
||||
const isEmailEnabled = extractDerivedDocumentEmailSettings(documentMeta).documentDeleted;
|
||||
|
||||
if (!isEmailEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const i18n = await getI18nInstance(documentMeta?.language);
|
||||
|
||||
// Send cancellation emails to all recipients who have been sent the document or viewed it
|
||||
const recipientsToNotify = document.recipients.filter(
|
||||
(recipient) =>
|
||||
(recipient.sendStatus === SendStatus.SENT || recipient.readStatus === ReadStatus.OPENED) &&
|
||||
recipient.signingStatus !== SigningStatus.REJECTED,
|
||||
);
|
||||
|
||||
await io.runTask('send-cancellation-emails', async () => {
|
||||
await Promise.all(
|
||||
recipientsToNotify.map(async (recipient) => {
|
||||
const template = createElement(DocumentCancelTemplate, {
|
||||
documentName: document.title,
|
||||
inviterName: documentOwner.name || undefined,
|
||||
inviterEmail: documentOwner.email,
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
cancellationReason: cancellationReason || 'The document has been cancelled.',
|
||||
});
|
||||
|
||||
const branding = document.team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: documentMeta?.language, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: documentMeta?.language,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
name: recipient.name,
|
||||
address: recipient.email,
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: i18n._(msg`Document "${document.title}" Cancelled`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION_ID = 'send.document.cancelled.emails';
|
||||
|
||||
const SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION_SCHEMA = z.object({
|
||||
documentId: z.number(),
|
||||
cancellationReason: z.string().optional(),
|
||||
requestMetadata: z.any().optional(),
|
||||
});
|
||||
|
||||
export type TSendDocumentCancelledEmailsJobDefinition = z.infer<
|
||||
typeof SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION = {
|
||||
id: SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION_ID,
|
||||
name: 'Send Document Cancelled Emails',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION_ID,
|
||||
schema: SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-document-cancelled-emails.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION_ID,
|
||||
TSendDocumentCancelledEmailsJobDefinition
|
||||
>;
|
||||
@@ -6,11 +6,9 @@ import { PDFDocument } from 'pdf-lib';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { signPdf } from '@documenso/signing';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../../errors/app-error';
|
||||
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
|
||||
import PostHogServerClient from '../../../server-only/feature-flags/get-post-hog-server-client';
|
||||
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
|
||||
import { addRejectionStampToPdf } from '../../../server-only/pdf/add-rejection-stamp-to-pdf';
|
||||
import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations';
|
||||
import { flattenForm } from '../../../server-only/pdf/flatten-form';
|
||||
import { insertFieldInPDF } from '../../../server-only/pdf/insert-field-in-pdf';
|
||||
@@ -24,7 +22,6 @@ import {
|
||||
import { getFileServerSide } from '../../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../../universal/upload/put-file.server';
|
||||
import { fieldsContainUnsignedRequiredField } from '../../../utils/advanced-fields-helpers';
|
||||
import { isDocumentCompleted } from '../../../utils/document';
|
||||
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSealDocumentJobDefinition } from './seal-document';
|
||||
@@ -41,6 +38,11 @@ export const run = async ({
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
recipients: {
|
||||
every: {
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
@@ -57,16 +59,6 @@ export const run = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const isComplete =
|
||||
document.recipients.some((recipient) => recipient.signingStatus === SigningStatus.REJECTED) ||
|
||||
document.recipients.every((recipient) => recipient.signingStatus === SigningStatus.SIGNED);
|
||||
|
||||
if (!isComplete) {
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Document is not complete',
|
||||
});
|
||||
}
|
||||
|
||||
// Seems silly but we need to do this in case the job is re-ran
|
||||
// after it has already run through the update task further below.
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
@@ -99,15 +91,9 @@ export const run = async ({
|
||||
},
|
||||
});
|
||||
|
||||
// Determine if the document has been rejected by checking if any recipient has rejected it
|
||||
const rejectedRecipient = recipients.find(
|
||||
(recipient) => recipient.signingStatus === SigningStatus.REJECTED,
|
||||
);
|
||||
|
||||
const isRejected = Boolean(rejectedRecipient);
|
||||
|
||||
// Get the rejection reason from the rejected recipient
|
||||
const rejectionReason = rejectedRecipient?.rejectionReason ?? '';
|
||||
if (recipients.some((recipient) => recipient.signingStatus !== SigningStatus.SIGNED)) {
|
||||
throw new Error(`Document ${document.id} has unsigned recipients`);
|
||||
}
|
||||
|
||||
const fields = await prisma.field.findMany({
|
||||
where: {
|
||||
@@ -118,8 +104,7 @@ export const run = async ({
|
||||
},
|
||||
});
|
||||
|
||||
// Skip the field check if the document is rejected
|
||||
if (!isRejected && fieldsContainUnsignedRequiredField(fields)) {
|
||||
if (fieldsContainUnsignedRequiredField(fields)) {
|
||||
throw new Error(`Document ${document.id} has unsigned required fields`);
|
||||
}
|
||||
|
||||
@@ -147,11 +132,6 @@ export const run = async ({
|
||||
flattenForm(pdfDoc);
|
||||
flattenAnnotations(pdfDoc);
|
||||
|
||||
// Add rejection stamp if the document is rejected
|
||||
if (isRejected && rejectionReason) {
|
||||
await addRejectionStampToPdf(pdfDoc, rejectionReason);
|
||||
}
|
||||
|
||||
if (certificateData) {
|
||||
const certificateDoc = await PDFDocument.load(certificateData);
|
||||
|
||||
@@ -180,11 +160,8 @@ export const run = async ({
|
||||
|
||||
const { name } = path.parse(document.title);
|
||||
|
||||
// Add suffix based on document status
|
||||
const suffix = isRejected ? '_rejected.pdf' : '_signed.pdf';
|
||||
|
||||
const documentData = await putPdfFileServerSide({
|
||||
name: `${name}${suffix}`,
|
||||
name: `${name}_signed.pdf`,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(pdfBuffer),
|
||||
});
|
||||
@@ -200,7 +177,6 @@ export const run = async ({
|
||||
event: 'App: Document Sealed',
|
||||
properties: {
|
||||
documentId: document.id,
|
||||
isRejected,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -218,7 +194,7 @@ export const run = async ({
|
||||
id: document.id,
|
||||
},
|
||||
data: {
|
||||
status: isRejected ? DocumentStatus.REJECTED : DocumentStatus.COMPLETED,
|
||||
status: DocumentStatus.COMPLETED,
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
@@ -240,7 +216,6 @@ export const run = async ({
|
||||
user: null,
|
||||
data: {
|
||||
transactionId: nanoid(),
|
||||
...(isRejected ? { isRejected: true, rejectionReason: rejectionReason } : {}),
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -248,9 +223,9 @@ export const run = async ({
|
||||
});
|
||||
|
||||
await io.runTask('send-completed-email', async () => {
|
||||
let shouldSendCompletedEmail = sendEmail && !isResealing && !isRejected;
|
||||
let shouldSendCompletedEmail = sendEmail && !isResealing;
|
||||
|
||||
if (isResealing && !isDocumentCompleted(document.status)) {
|
||||
if (isResealing && documentStatus !== DocumentStatus.COMPLETED) {
|
||||
shouldSendCompletedEmail = sendEmail;
|
||||
}
|
||||
|
||||
@@ -271,9 +246,7 @@ export const run = async ({
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: isRejected
|
||||
? WebhookTriggerEvents.DOCUMENT_REJECTED
|
||||
: WebhookTriggerEvents.DOCUMENT_COMPLETED,
|
||||
event: WebhookTriggerEvents.DOCUMENT_COMPLETED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
||||
userId: updatedDocument.userId,
|
||||
teamId: updatedDocument.teamId ?? undefined,
|
||||
|
||||
@@ -13,7 +13,6 @@ export const getDocumentStats = async () => {
|
||||
[ExtendedDocumentStatus.DRAFT]: 0,
|
||||
[ExtendedDocumentStatus.PENDING]: 0,
|
||||
[ExtendedDocumentStatus.COMPLETED]: 0,
|
||||
[ExtendedDocumentStatus.REJECTED]: 0,
|
||||
[ExtendedDocumentStatus.ALL]: 0,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DocumentStatus, SubscriptionStatus } from '@prisma/client';
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
|
||||
import { kyselyPrisma, sql } from '@documenso/prisma';
|
||||
|
||||
@@ -44,6 +44,7 @@ export async function getSigningVolume({
|
||||
.on('td.status', '=', sql.lit(DocumentStatus.COMPLETED))
|
||||
.on('td.deletedAt', 'is', null),
|
||||
)
|
||||
// @ts-expect-error - Raw SQL enum casting not properly typed by Kysely
|
||||
.where(sql`s.status = ${SubscriptionStatus.ACTIVE}::"SubscriptionStatus"`)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
@@ -81,6 +82,7 @@ export async function getSigningVolume({
|
||||
.selectFrom('Subscription as s')
|
||||
.leftJoin('User as u', 's.userId', 'u.id')
|
||||
.leftJoin('Team as t', 's.teamId', 't.id')
|
||||
// @ts-expect-error - Raw SQL enum casting not properly typed by Kysely
|
||||
.where(sql`s.status = ${SubscriptionStatus.ACTIVE}::"SubscriptionStatus"`)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
|
||||
@@ -24,7 +24,6 @@ export type CreateDocumentMetaOptions = {
|
||||
redirectUrl?: string;
|
||||
emailSettings?: TDocumentEmailSettings;
|
||||
signingOrder?: DocumentSigningOrder;
|
||||
allowDictateNextSigner?: boolean;
|
||||
distributionMethod?: DocumentDistributionMethod;
|
||||
typedSignatureEnabled?: boolean;
|
||||
language?: SupportedLanguageCodes;
|
||||
@@ -42,7 +41,6 @@ export const upsertDocumentMeta = async ({
|
||||
password,
|
||||
redirectUrl,
|
||||
signingOrder,
|
||||
allowDictateNextSigner,
|
||||
emailSettings,
|
||||
distributionMethod,
|
||||
typedSignatureEnabled,
|
||||
@@ -95,7 +93,6 @@ export const upsertDocumentMeta = async ({
|
||||
documentId,
|
||||
redirectUrl,
|
||||
signingOrder,
|
||||
allowDictateNextSigner,
|
||||
emailSettings,
|
||||
distributionMethod,
|
||||
typedSignatureEnabled,
|
||||
@@ -109,7 +106,6 @@ export const upsertDocumentMeta = async ({
|
||||
timezone,
|
||||
redirectUrl,
|
||||
signingOrder,
|
||||
allowDictateNextSigner,
|
||||
emailSettings,
|
||||
distributionMethod,
|
||||
typedSignatureEnabled,
|
||||
|
||||
@@ -7,10 +7,7 @@ import {
|
||||
WebhookTriggerEvents,
|
||||
} from '@prisma/client';
|
||||
|
||||
import {
|
||||
DOCUMENT_AUDIT_LOG_TYPE,
|
||||
RECIPIENT_DIFF_TYPE,
|
||||
} from '@documenso/lib/types/document-audit-logs';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
@@ -33,10 +30,6 @@ export type CompleteDocumentWithTokenOptions = {
|
||||
userId?: number;
|
||||
authOptions?: TRecipientActionAuth;
|
||||
requestMetadata?: RequestMetadata;
|
||||
nextSigner?: {
|
||||
email: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
||||
const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptions) => {
|
||||
@@ -64,7 +57,6 @@ export const completeDocumentWithToken = async ({
|
||||
token,
|
||||
documentId,
|
||||
requestMetadata,
|
||||
nextSigner,
|
||||
}: CompleteDocumentWithTokenOptions) => {
|
||||
const document = await getDocument({ token, documentId });
|
||||
|
||||
@@ -154,6 +146,7 @@ export const completeDocumentWithToken = async ({
|
||||
recipientName: recipient.name,
|
||||
recipientId: recipient.id,
|
||||
recipientRole: recipient.role,
|
||||
// actionAuth: derivedRecipientActionAuth || undefined,
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -171,9 +164,6 @@ export const completeDocumentWithToken = async ({
|
||||
select: {
|
||||
id: true,
|
||||
signingOrder: true,
|
||||
name: true,
|
||||
email: true,
|
||||
role: true,
|
||||
},
|
||||
where: {
|
||||
documentId: document.id,
|
||||
@@ -196,49 +186,9 @@ export const completeDocumentWithToken = async ({
|
||||
const [nextRecipient] = pendingRecipients;
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
if (nextSigner && document.documentMeta?.allowDictateNextSigner) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
||||
documentId: document.id,
|
||||
user: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
},
|
||||
requestMetadata,
|
||||
data: {
|
||||
recipientEmail: nextRecipient.email,
|
||||
recipientName: nextRecipient.name,
|
||||
recipientId: nextRecipient.id,
|
||||
recipientRole: nextRecipient.role,
|
||||
changes: [
|
||||
{
|
||||
type: RECIPIENT_DIFF_TYPE.NAME,
|
||||
from: nextRecipient.name,
|
||||
to: nextSigner.name,
|
||||
},
|
||||
{
|
||||
type: RECIPIENT_DIFF_TYPE.EMAIL,
|
||||
from: nextRecipient.email,
|
||||
to: nextSigner.email,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
await tx.recipient.update({
|
||||
where: { id: nextRecipient.id },
|
||||
data: {
|
||||
sendStatus: SendStatus.SENT,
|
||||
...(nextSigner && document.documentMeta?.allowDictateNextSigner
|
||||
? {
|
||||
name: nextSigner.name,
|
||||
email: nextSigner.email,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
data: { sendStatus: SendStatus.SENT },
|
||||
});
|
||||
|
||||
await jobs.triggerJob({
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { isDocumentCompleted } from '../../utils/document';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
@@ -162,7 +161,7 @@ const handleDocumentOwnerDelete = async ({
|
||||
}
|
||||
|
||||
// Soft delete completed documents.
|
||||
if (isDocumentCompleted(document.status)) {
|
||||
if (document.status === DocumentStatus.COMPLETED) {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
|
||||
@@ -356,24 +356,6 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => {
|
||||
},
|
||||
],
|
||||
}))
|
||||
.with(ExtendedDocumentStatus.REJECTED, () => ({
|
||||
OR: [
|
||||
{
|
||||
userId: user.id,
|
||||
teamId: null,
|
||||
status: ExtendedDocumentStatus.REJECTED,
|
||||
},
|
||||
{
|
||||
status: ExtendedDocumentStatus.REJECTED,
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
signingStatus: SigningStatus.REJECTED,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
.exhaustive();
|
||||
};
|
||||
|
||||
@@ -566,38 +548,5 @@ const findTeamDocumentsFilter = (
|
||||
|
||||
return filter;
|
||||
})
|
||||
.with(ExtendedDocumentStatus.REJECTED, () => {
|
||||
const filter: Prisma.DocumentWhereInput = {
|
||||
status: ExtendedDocumentStatus.REJECTED,
|
||||
OR: [
|
||||
{
|
||||
teamId: team.id,
|
||||
OR: visibilityFilters,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (teamEmail && filter.OR) {
|
||||
filter.OR.push(
|
||||
{
|
||||
recipients: {
|
||||
some: {
|
||||
email: teamEmail,
|
||||
signingStatus: SigningStatus.REJECTED,
|
||||
},
|
||||
},
|
||||
OR: visibilityFilters,
|
||||
},
|
||||
{
|
||||
user: {
|
||||
email: teamEmail,
|
||||
},
|
||||
OR: visibilityFilters,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return filter;
|
||||
})
|
||||
.exhaustive();
|
||||
};
|
||||
|
||||
@@ -16,7 +16,6 @@ export const getDocumentCertificateAuditLogs = async ({
|
||||
type: {
|
||||
in: [
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED,
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
||||
DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
],
|
||||
@@ -30,9 +29,6 @@ export const getDocumentCertificateAuditLogs = async ({
|
||||
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED]: auditLogs.filter(
|
||||
(log) => log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||
),
|
||||
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED]: auditLogs.filter(
|
||||
(log) => log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED,
|
||||
),
|
||||
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED]: auditLogs.filter(
|
||||
(log) => log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
||||
),
|
||||
|
||||
@@ -44,7 +44,6 @@ export const getStats = async ({ user, period, search = '', ...options }: GetSta
|
||||
[ExtendedDocumentStatus.DRAFT]: 0,
|
||||
[ExtendedDocumentStatus.PENDING]: 0,
|
||||
[ExtendedDocumentStatus.COMPLETED]: 0,
|
||||
[ExtendedDocumentStatus.REJECTED]: 0,
|
||||
[ExtendedDocumentStatus.INBOX]: 0,
|
||||
[ExtendedDocumentStatus.ALL]: 0,
|
||||
};
|
||||
@@ -65,10 +64,6 @@ export const getStats = async ({ user, period, search = '', ...options }: GetSta
|
||||
if (stat.status === ExtendedDocumentStatus.PENDING) {
|
||||
stats[ExtendedDocumentStatus.PENDING] += stat._count._all;
|
||||
}
|
||||
|
||||
if (stat.status === ExtendedDocumentStatus.REJECTED) {
|
||||
stats[ExtendedDocumentStatus.REJECTED] += stat._count._all;
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(stats).forEach((key) => {
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
import { WebhookTriggerEvents } from '@prisma/client';
|
||||
|
||||
import { jobs } from '@documenso/lib/jobs/client';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type RejectDocumentWithTokenOptions = {
|
||||
token: string;
|
||||
@@ -78,16 +84,7 @@ export async function rejectDocumentWithToken({
|
||||
}),
|
||||
]);
|
||||
|
||||
// Trigger the seal document job to process the document asynchronously
|
||||
await jobs.triggerJob({
|
||||
name: 'internal.seal-document',
|
||||
payload: {
|
||||
documentId,
|
||||
requestMetadata,
|
||||
},
|
||||
});
|
||||
|
||||
// Send email notifications to the rejecting recipient
|
||||
// Send email notifications
|
||||
await jobs.triggerJob({
|
||||
name: 'send.signing.rejected.emails',
|
||||
payload: {
|
||||
@@ -96,14 +93,27 @@ export async function rejectDocumentWithToken({
|
||||
},
|
||||
});
|
||||
|
||||
// Send cancellation emails to other recipients
|
||||
await jobs.triggerJob({
|
||||
name: 'send.document.cancelled.emails',
|
||||
payload: {
|
||||
documentId,
|
||||
cancellationReason: reason,
|
||||
requestMetadata,
|
||||
// Get the updated document with all recipients
|
||||
const updatedDocument = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: document.id,
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!updatedDocument) {
|
||||
throw new Error('Document not found after update');
|
||||
}
|
||||
|
||||
// Trigger webhook for document rejection
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_REJECTED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
||||
userId: document.userId,
|
||||
teamId: document.teamId ?? undefined,
|
||||
});
|
||||
|
||||
return updatedRecipient;
|
||||
|
||||
@@ -20,7 +20,6 @@ import { prisma } from '@documenso/prisma';
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { isDocumentCompleted } from '../../utils/document';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
@@ -89,7 +88,7 @@ export const resendDocument = async ({
|
||||
throw new Error('Can not send draft document');
|
||||
}
|
||||
|
||||
if (isDocumentCompleted(document.status)) {
|
||||
if (document.status === DocumentStatus.COMPLETED) {
|
||||
throw new Error('Can not send completed document');
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers';
|
||||
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
|
||||
import { addRejectionStampToPdf } from '../pdf/add-rejection-stamp-to-pdf';
|
||||
import { flattenAnnotations } from '../pdf/flatten-annotations';
|
||||
import { flattenForm } from '../pdf/flatten-form';
|
||||
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
|
||||
@@ -42,6 +41,11 @@ export const sealDocument = async ({
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
recipients: {
|
||||
every: {
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
@@ -74,21 +78,7 @@ export const sealDocument = async ({
|
||||
},
|
||||
});
|
||||
|
||||
// Determine if the document has been rejected by checking if any recipient has rejected it
|
||||
const rejectedRecipient = recipients.find(
|
||||
(recipient) => recipient.signingStatus === SigningStatus.REJECTED,
|
||||
);
|
||||
|
||||
const isRejected = Boolean(rejectedRecipient);
|
||||
|
||||
// Get the rejection reason from the rejected recipient
|
||||
const rejectionReason = rejectedRecipient?.rejectionReason ?? '';
|
||||
|
||||
// If the document is not rejected, ensure all recipients have signed
|
||||
if (
|
||||
!isRejected &&
|
||||
recipients.some((recipient) => recipient.signingStatus !== SigningStatus.SIGNED)
|
||||
) {
|
||||
if (recipients.some((recipient) => recipient.signingStatus !== SigningStatus.SIGNED)) {
|
||||
throw new Error(`Document ${document.id} has unsigned recipients`);
|
||||
}
|
||||
|
||||
@@ -101,8 +91,7 @@ export const sealDocument = async ({
|
||||
},
|
||||
});
|
||||
|
||||
// Skip the field check if the document is rejected
|
||||
if (!isRejected && fieldsContainUnsignedRequiredField(fields)) {
|
||||
if (fieldsContainUnsignedRequiredField(fields)) {
|
||||
throw new Error(`Document ${document.id} has unsigned required fields`);
|
||||
}
|
||||
|
||||
@@ -130,11 +119,6 @@ export const sealDocument = async ({
|
||||
flattenForm(doc);
|
||||
flattenAnnotations(doc);
|
||||
|
||||
// Add rejection stamp if the document is rejected
|
||||
if (isRejected && rejectionReason) {
|
||||
await addRejectionStampToPdf(doc, rejectionReason);
|
||||
}
|
||||
|
||||
if (certificateData) {
|
||||
const certificate = await PDFDocument.load(certificateData);
|
||||
|
||||
@@ -158,11 +142,8 @@ export const sealDocument = async ({
|
||||
|
||||
const { name } = path.parse(document.title);
|
||||
|
||||
// Add suffix based on document status
|
||||
const suffix = isRejected ? '_rejected.pdf' : '_signed.pdf';
|
||||
|
||||
const { data: newData } = await putPdfFileServerSide({
|
||||
name: `${name}${suffix}`,
|
||||
name: `${name}_signed.pdf`,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(pdfBuffer),
|
||||
});
|
||||
@@ -175,7 +156,6 @@ export const sealDocument = async ({
|
||||
event: 'App: Document Sealed',
|
||||
properties: {
|
||||
documentId: document.id,
|
||||
isRejected,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -186,7 +166,7 @@ export const sealDocument = async ({
|
||||
id: document.id,
|
||||
},
|
||||
data: {
|
||||
status: isRejected ? DocumentStatus.REJECTED : DocumentStatus.COMPLETED,
|
||||
status: DocumentStatus.COMPLETED,
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
@@ -208,7 +188,6 @@ export const sealDocument = async ({
|
||||
user: null,
|
||||
data: {
|
||||
transactionId: nanoid(),
|
||||
...(isRejected ? { isRejected: true, rejectionReason } : {}),
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -230,9 +209,7 @@ export const sealDocument = async ({
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: isRejected
|
||||
? WebhookTriggerEvents.DOCUMENT_REJECTED
|
||||
: WebhookTriggerEvents.DOCUMENT_COMPLETED,
|
||||
event: WebhookTriggerEvents.DOCUMENT_COMPLETED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
||||
userId: document.userId,
|
||||
teamId: document.teamId ?? undefined,
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
} from '../../types/webhook-payload';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { isDocumentCompleted } from '../../utils/document';
|
||||
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
@@ -75,7 +74,7 @@ export const sendDocument = async ({
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
if (isDocumentCompleted(document.status)) {
|
||||
if (document.status === DocumentStatus.COMPLETED) {
|
||||
throw new Error('Can not send completed document');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import fontkit from '@pdf-lib/fontkit';
|
||||
import type { PDFDocument } from 'pdf-lib';
|
||||
import { TextAlignment, rgb, setFontAndSize } from 'pdf-lib';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
|
||||
/**
|
||||
* Adds a rejection stamp to each page of a PDF document.
|
||||
* The stamp is placed in the center of the page.
|
||||
*/
|
||||
export async function addRejectionStampToPdf(
|
||||
pdfDoc: PDFDocument,
|
||||
reason: string,
|
||||
): Promise<PDFDocument> {
|
||||
const pages = pdfDoc.getPages();
|
||||
pdfDoc.registerFontkit(fontkit);
|
||||
|
||||
const fontBytes = await fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(
|
||||
async (res) => res.arrayBuffer(),
|
||||
);
|
||||
|
||||
const font = await pdfDoc.embedFont(fontBytes, {
|
||||
customName: 'Noto',
|
||||
});
|
||||
|
||||
const form = pdfDoc.getForm();
|
||||
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const page = pages[i];
|
||||
const { width, height } = page.getSize();
|
||||
|
||||
// Draw the "REJECTED" text
|
||||
const rejectedTitleText = 'DOCUMENT REJECTED';
|
||||
const rejectedTitleFontSize = 36;
|
||||
const rejectedTitleTextField = form.createTextField(`internal-document-rejected-title-${i}`);
|
||||
|
||||
if (!rejectedTitleTextField.acroField.getDefaultAppearance()) {
|
||||
rejectedTitleTextField.acroField.setDefaultAppearance(
|
||||
setFontAndSize('Noto', rejectedTitleFontSize).toString(),
|
||||
);
|
||||
}
|
||||
|
||||
rejectedTitleTextField.updateAppearances(font);
|
||||
|
||||
rejectedTitleTextField.setFontSize(rejectedTitleFontSize);
|
||||
rejectedTitleTextField.setText(rejectedTitleText);
|
||||
rejectedTitleTextField.setAlignment(TextAlignment.Center);
|
||||
|
||||
const rejectedTitleTextWidth =
|
||||
font.widthOfTextAtSize(rejectedTitleText, rejectedTitleFontSize) * 1.2;
|
||||
const rejectedTitleTextHeight = font.heightAtSize(rejectedTitleFontSize);
|
||||
|
||||
// Calculate the center position of the page
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
|
||||
// Position the title text at the center of the page
|
||||
const rejectedTitleTextX = centerX - rejectedTitleTextWidth / 2;
|
||||
const rejectedTitleTextY = centerY - rejectedTitleTextHeight / 2;
|
||||
|
||||
// Add padding for the rectangle
|
||||
const padding = 20;
|
||||
|
||||
// Draw the stamp background
|
||||
page.drawRectangle({
|
||||
x: rejectedTitleTextX - padding / 2,
|
||||
y: rejectedTitleTextY - padding / 2,
|
||||
width: rejectedTitleTextWidth + padding,
|
||||
height: rejectedTitleTextHeight + padding,
|
||||
borderColor: rgb(220 / 255, 38 / 255, 38 / 255),
|
||||
borderWidth: 4,
|
||||
});
|
||||
|
||||
rejectedTitleTextField.addToPage(page, {
|
||||
x: rejectedTitleTextX,
|
||||
y: rejectedTitleTextY,
|
||||
width: rejectedTitleTextWidth,
|
||||
height: rejectedTitleTextHeight,
|
||||
textColor: rgb(220 / 255, 38 / 255, 38 / 255),
|
||||
backgroundColor: undefined,
|
||||
borderWidth: 0,
|
||||
borderColor: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return pdfDoc;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export const getNextPendingRecipient = async ({
|
||||
documentId,
|
||||
currentRecipientId,
|
||||
}: {
|
||||
documentId: number;
|
||||
currentRecipientId: number;
|
||||
}) => {
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
documentId,
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
signingOrder: {
|
||||
sort: 'asc',
|
||||
nulls: 'last',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'asc',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const currentIndex = recipients.findIndex((r) => r.id === currentRecipientId);
|
||||
|
||||
if (currentIndex === -1 || currentIndex === recipients.length - 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...recipients[currentIndex + 1],
|
||||
token: '',
|
||||
};
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { z } from 'zod';
|
||||
import { createTeamCustomer } from '@documenso/ee/server-only/stripe/create-team-customer';
|
||||
import { getTeamRelatedPrices } from '@documenso/ee/server-only/stripe/get-team-related-prices';
|
||||
import { mapStripeSubscriptionToPrismaUpsertAction } from '@documenso/ee/server-only/stripe/webhook/on-subscription-updated';
|
||||
import { isDocumentPlatform as isUserPlatformPlan } from '@documenso/ee/server-only/util/is-document-platform';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
|
||||
@@ -60,6 +61,11 @@ export const createTeam = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const isPlatformPlan = await isUserPlatformPlan({
|
||||
userId: user.id,
|
||||
teamId: null,
|
||||
});
|
||||
|
||||
let isPaymentRequired = IS_BILLING_ENABLED();
|
||||
let customerId: string | null = null;
|
||||
|
||||
@@ -68,7 +74,25 @@ export const createTeam = async ({
|
||||
prices.map((price) => price.id),
|
||||
);
|
||||
|
||||
isPaymentRequired = !subscriptionsContainsActivePlan(user.subscriptions, teamRelatedPriceIds);
|
||||
const hasTeamRelatedSubscription = subscriptionsContainsActivePlan(
|
||||
user.subscriptions,
|
||||
teamRelatedPriceIds,
|
||||
);
|
||||
|
||||
if (isPlatformPlan) {
|
||||
// For platform users, check if they already have any teams
|
||||
const existingTeams = await prisma.team.findMany({
|
||||
where: {
|
||||
ownerUserId: userId,
|
||||
},
|
||||
});
|
||||
|
||||
// Payment is required if they already have any team
|
||||
isPaymentRequired = existingTeams.length > 0;
|
||||
} else {
|
||||
// For non-platform users, payment is required if they don't have a team-related subscription
|
||||
isPaymentRequired = !hasTeamRelatedSubscription;
|
||||
}
|
||||
|
||||
customerId = await createTeamCustomer({
|
||||
name: user.name ?? teamName,
|
||||
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { isRequiredField } from '../../utils/advanced-fields-helpers';
|
||||
import type { CreateDocumentAuditLogDataResponse } from '../../utils/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import {
|
||||
@@ -177,28 +176,20 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
const metaSigningOrder = template.templateMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
|
||||
|
||||
// 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(
|
||||
fieldsToProcess.map(async (templateField) => {
|
||||
directTemplateRecipient.fields.map(async (templateField) => {
|
||||
const signedFieldValue = signedFieldValues.find(
|
||||
(value) => value.fieldId === templateField.id,
|
||||
);
|
||||
|
||||
if (isRequiredField(templateField) && !signedFieldValue) {
|
||||
if (!signedFieldValue) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid, missing or changed fields',
|
||||
});
|
||||
}
|
||||
|
||||
if (templateField.type === FieldType.NAME && directRecipientName === undefined) {
|
||||
directRecipientName === signedFieldValue?.value;
|
||||
directRecipientName === signedFieldValue.value;
|
||||
}
|
||||
|
||||
const derivedRecipientActionAuth = await validateFieldAuth({
|
||||
@@ -209,18 +200,9 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
},
|
||||
field: templateField,
|
||||
userId: user?.id,
|
||||
authOptions: signedFieldValue?.authOptions,
|
||||
authOptions: signedFieldValue.authOptions,
|
||||
});
|
||||
|
||||
if (!signedFieldValue) {
|
||||
return {
|
||||
templateField,
|
||||
customText: '',
|
||||
derivedRecipientActionAuth,
|
||||
signature: null,
|
||||
};
|
||||
}
|
||||
|
||||
const { value, isBase64 } = signedFieldValue;
|
||||
|
||||
const isSignatureField =
|
||||
@@ -398,7 +380,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
positionY: templateField.positionY,
|
||||
width: templateField.width,
|
||||
height: templateField.height,
|
||||
customText: customText ?? '',
|
||||
customText,
|
||||
inserted: true,
|
||||
fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
|
||||
})),
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
@@ -19,20 +18,7 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { ZRecipientAuthOptionsSchema } from '../../types/document-auth';
|
||||
import type { TDocumentEmailSettings } from '../../types/document-email';
|
||||
import type {
|
||||
TCheckboxFieldMeta,
|
||||
TDropdownFieldMeta,
|
||||
TFieldMetaPrefillFieldsSchema,
|
||||
TNumberFieldMeta,
|
||||
TRadioFieldMeta,
|
||||
TTextFieldMeta,
|
||||
} from '../../types/field-meta';
|
||||
import {
|
||||
ZCheckboxFieldMeta,
|
||||
ZDropdownFieldMeta,
|
||||
ZFieldMetaSchema,
|
||||
ZRadioFieldMeta,
|
||||
} from '../../types/field-meta';
|
||||
import { ZFieldMetaSchema } from '../../types/field-meta';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
@@ -65,7 +51,6 @@ export type CreateDocumentFromTemplateOptions = {
|
||||
email: string;
|
||||
signingOrder?: number | null;
|
||||
}[];
|
||||
prefillFields?: TFieldMetaPrefillFieldsSchema[];
|
||||
customDocumentDataId?: string;
|
||||
|
||||
/**
|
||||
@@ -83,181 +68,11 @@ export type CreateDocumentFromTemplateOptions = {
|
||||
language?: SupportedLanguageCodes;
|
||||
distributionMethod?: DocumentDistributionMethod;
|
||||
typedSignatureEnabled?: boolean;
|
||||
allowDictateNextSigner?: boolean;
|
||||
emailSettings?: TDocumentEmailSettings;
|
||||
};
|
||||
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 ({
|
||||
templateId,
|
||||
externalId,
|
||||
@@ -267,7 +82,6 @@ export const createDocumentFromTemplate = async ({
|
||||
customDocumentDataId,
|
||||
override,
|
||||
requestMetadata,
|
||||
prefillFields,
|
||||
}: CreateDocumentFromTemplateOptions) => {
|
||||
const template = await prisma.template.findUnique({
|
||||
where: {
|
||||
@@ -405,10 +219,6 @@ export const createDocumentFromTemplate = async ({
|
||||
template.team?.teamGlobalSettings?.documentLanguage,
|
||||
typedSignatureEnabled:
|
||||
override?.typedSignatureEnabled ?? template.templateMeta?.typedSignatureEnabled,
|
||||
allowDictateNextSigner:
|
||||
override?.allowDictateNextSigner ??
|
||||
template.templateMeta?.allowDictateNextSigner ??
|
||||
false,
|
||||
},
|
||||
},
|
||||
recipients: {
|
||||
@@ -449,47 +259,6 @@ export const createDocumentFromTemplate = async ({
|
||||
|
||||
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 }) => {
|
||||
const recipient = document.recipients.find((recipient) => recipient.email === email);
|
||||
|
||||
@@ -498,25 +267,19 @@ export const createDocumentFromTemplate = async ({
|
||||
}
|
||||
|
||||
fieldsToCreate = fieldsToCreate.concat(
|
||||
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,
|
||||
recipientId: recipient.id,
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
positionX: field.positionX,
|
||||
positionY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: updatedFieldMeta,
|
||||
};
|
||||
}),
|
||||
fields.map((field) => ({
|
||||
documentId: document.id,
|
||||
recipientId: recipient.id,
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
positionX: field.positionX,
|
||||
positionY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta,
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ export const deleteUser = async ({ id }: DeleteUserOptions) => {
|
||||
where: {
|
||||
userId: user.id,
|
||||
status: {
|
||||
in: [DocumentStatus.PENDING, DocumentStatus.REJECTED, DocumentStatus.COMPLETED],
|
||||
in: [DocumentStatus.PENDING, DocumentStatus.COMPLETED],
|
||||
},
|
||||
},
|
||||
data: {
|
||||
|
||||
@@ -21,16 +21,14 @@ export const handlerTriggerWebhooks = async (req: Request) => {
|
||||
return Response.json({ success: false, error: 'Missing signature' }, { status: 400 });
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
|
||||
const valid = verify(body, signature);
|
||||
const valid = verify(req.body, signature);
|
||||
|
||||
if (!valid) {
|
||||
console.log('Invalid signature');
|
||||
return Response.json({ success: false, error: 'Invalid signature' }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = ZTriggerWebhookBodySchema.safeParse(body);
|
||||
const result = ZTriggerWebhookBodySchema.safeParse(req.body);
|
||||
|
||||
if (!result.success) {
|
||||
console.log('Invalid request body');
|
||||
|
||||
@@ -473,7 +473,6 @@ 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?"
|
||||
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
|
||||
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?"
|
||||
@@ -1145,7 +1144,6 @@ msgstr "App-Version"
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.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/document-page-view-button.tsx
|
||||
#: packages/lib/constants/recipient-roles.ts
|
||||
msgid "Approve"
|
||||
@@ -1567,7 +1565,6 @@ msgstr "Klicken, um das Feld auszufüllen"
|
||||
msgid "Close"
|
||||
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/forms/signup.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
@@ -1579,10 +1576,6 @@ msgstr "Abschließen"
|
||||
msgid "Complete Approval"
|
||||
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
|
||||
msgid "Complete Document"
|
||||
msgstr ""
|
||||
@@ -1595,7 +1588,6 @@ msgstr "Unterzeichnung abschließen"
|
||||
msgid "Complete the fields for the following signers. Once reviewed, they will inform you if any modifications are needed."
|
||||
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
|
||||
msgid "Complete Viewing"
|
||||
msgstr "Betrachten abschließen"
|
||||
@@ -3381,11 +3373,6 @@ msgstr "Verwalten Sie hier Ihre Seiteneinstellungen"
|
||||
msgid "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
|
||||
msgid "Mark as Viewed"
|
||||
msgstr "Als angesehen markieren"
|
||||
@@ -4001,10 +3988,6 @@ 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."
|
||||
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
|
||||
msgid "Please review the document before signing."
|
||||
msgstr "Bitte überprüfen Sie das Dokument vor der Unterzeichnung."
|
||||
|
||||
@@ -468,7 +468,6 @@ 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?"
|
||||
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
|
||||
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?"
|
||||
@@ -1140,7 +1139,6 @@ msgstr "App Version"
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.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/document-page-view-button.tsx
|
||||
#: packages/lib/constants/recipient-roles.ts
|
||||
msgid "Approve"
|
||||
@@ -1562,7 +1560,6 @@ msgstr "Click to insert field"
|
||||
msgid "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/forms/signup.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
@@ -1574,10 +1571,6 @@ msgstr "Complete"
|
||||
msgid "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
|
||||
msgid "Complete Document"
|
||||
msgstr "Complete Document"
|
||||
@@ -1590,7 +1583,6 @@ msgstr "Complete Signing"
|
||||
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."
|
||||
|
||||
#: 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"
|
||||
msgstr "Complete Viewing"
|
||||
@@ -3376,11 +3368,6 @@ msgstr "Manage your site settings here"
|
||||
msgid "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
|
||||
msgid "Mark as Viewed"
|
||||
msgstr "Mark as Viewed"
|
||||
@@ -3996,10 +3983,6 @@ 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."
|
||||
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
|
||||
msgid "Please review the document before signing."
|
||||
msgstr "Please review the document before signing."
|
||||
|
||||
@@ -473,7 +473,6 @@ 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?"
|
||||
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
|
||||
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?"
|
||||
@@ -1145,7 +1144,6 @@ 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-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/document-page-view-button.tsx
|
||||
#: packages/lib/constants/recipient-roles.ts
|
||||
msgid "Approve"
|
||||
@@ -1567,7 +1565,6 @@ msgstr "Haga clic para insertar campo"
|
||||
msgid "Close"
|
||||
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/forms/signup.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
@@ -1579,10 +1576,6 @@ msgstr "Completo"
|
||||
msgid "Complete Approval"
|
||||
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
|
||||
msgid "Complete Document"
|
||||
msgstr ""
|
||||
@@ -1595,7 +1588,6 @@ msgstr "Completar Firmado"
|
||||
msgid "Complete the fields for the following signers. Once reviewed, they will inform you if any modifications are needed."
|
||||
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
|
||||
msgid "Complete Viewing"
|
||||
msgstr "Completar Visualización"
|
||||
@@ -3381,11 +3373,6 @@ msgstr "Gestionar la configuración de tu sitio aquí"
|
||||
msgid "Manager"
|
||||
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
|
||||
msgid "Mark as Viewed"
|
||||
msgstr "Marcar como visto"
|
||||
@@ -4001,10 +3988,6 @@ 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."
|
||||
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
|
||||
msgid "Please review the document before signing."
|
||||
msgstr "Por favor, revise el documento antes de firmar."
|
||||
|
||||
@@ -417,7 +417,7 @@ msgstr "<0>{teamName}</0> a demandé à utiliser votre adresse e-mail pour leur
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
|
||||
msgid "<0>Click to upload</0> or drag and drop"
|
||||
msgstr "<0>Cliquez pour importer</0> ou faites glisser et déposez"
|
||||
msgstr "<0>Cliquez pour télécharger</0> ou faites glisser et déposez"
|
||||
|
||||
#: packages/ui/primitives/template-flow/add-template-settings.tsx
|
||||
msgid "<0>Email</0> - The recipient will be emailed the document to sign, approve, etc."
|
||||
@@ -473,7 +473,6 @@ 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?"
|
||||
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
|
||||
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 ?"
|
||||
@@ -1082,7 +1081,7 @@ msgstr "Une erreur est survenue lors de la mise à jour de votre profil."
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload.tsx
|
||||
msgid "An error occurred while uploading your document."
|
||||
msgstr "Une erreur est survenue lors de l'importation de votre document."
|
||||
msgstr "Une erreur est survenue lors du téléchargement de votre document."
|
||||
|
||||
#: apps/remix/app/components/general/generic-error-layout.tsx
|
||||
msgid "An unexpected error occurred."
|
||||
@@ -1145,7 +1144,6 @@ msgstr "Version de l'application"
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.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/document-page-view-button.tsx
|
||||
#: packages/lib/constants/recipient-roles.ts
|
||||
msgid "Approve"
|
||||
@@ -1539,7 +1537,7 @@ msgstr "Cliquez ici pour réessayer"
|
||||
|
||||
#: apps/remix/app/components/dialogs/team-member-invite-dialog.tsx
|
||||
msgid "Click here to upload"
|
||||
msgstr "Cliquez ici pour importer"
|
||||
msgstr "Cliquez ici pour télécharger"
|
||||
|
||||
#: apps/remix/app/components/general/avatar-with-recipient.tsx
|
||||
#: apps/remix/app/components/general/avatar-with-recipient.tsx
|
||||
@@ -1567,7 +1565,6 @@ msgstr "Cliquez pour insérer un champ"
|
||||
msgid "Close"
|
||||
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/forms/signup.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
@@ -1579,10 +1576,6 @@ msgstr "Compléter"
|
||||
msgid "Complete Approval"
|
||||
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
|
||||
msgid "Complete Document"
|
||||
msgstr ""
|
||||
@@ -1595,7 +1588,6 @@ msgstr "Compléter la signature"
|
||||
msgid "Complete the fields for the following signers. Once reviewed, they will inform you if any modifications are needed."
|
||||
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
|
||||
msgid "Complete Viewing"
|
||||
msgstr "Compléter la visualisation"
|
||||
@@ -1727,11 +1719,11 @@ msgstr "Continuer vers la connexion"
|
||||
|
||||
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
|
||||
msgid "Controls the default language of an uploaded document. This will be used as the language in email communications with the recipients."
|
||||
msgstr "Contrôle la langue par défaut d'un document importé. Cela sera utilisé comme langue dans les communications par e-mail avec les destinataires."
|
||||
msgstr "Contrôle la langue par défaut d'un document téléchargé. Cela sera utilisé comme langue dans les communications par e-mail avec les destinataires."
|
||||
|
||||
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
|
||||
msgid "Controls the default visibility of an uploaded document."
|
||||
msgstr "Contrôle la visibilité par défaut d'un document importé."
|
||||
msgstr "Contrôle la visibilité par défaut d'un document téléchargé."
|
||||
|
||||
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
|
||||
msgid "Controls the formatting of the message that will be sent when inviting a recipient to sign a document. If a custom message has been provided while configuring the document, it will be used instead."
|
||||
@@ -2394,11 +2386,11 @@ msgstr "Document mis à jour"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload.tsx
|
||||
msgid "Document upload disabled due to unpaid invoices"
|
||||
msgstr "Importation de documents désactivé en raison de factures impayées"
|
||||
msgstr "Téléchargement du document désactivé en raison de factures impayées"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload.tsx
|
||||
msgid "Document uploaded"
|
||||
msgstr "Document importé"
|
||||
msgstr "Document téléchargé"
|
||||
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
|
||||
msgid "Document Viewed"
|
||||
@@ -2860,7 +2852,7 @@ msgstr "Pour toute question concernant cette divulgation, les signatures électr
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
||||
msgid "For each recipient, provide their email (required) and name (optional) in separate columns. Download the template CSV below for the correct format."
|
||||
msgstr "Pour chaque destinataire, fournissez son e-mail (obligatoire) et son nom (facultatif) dans des colonnes séparées. Téléchargez le modèle CSV ci-dessous pour obtenir le format requis."
|
||||
msgstr "Pour chaque destinataire, fournissez leur email (obligatoire) et leur nom (facultatif) dans des colonnes séparées. Téléchargez le modèle CSV ci-dessous pour le format correct."
|
||||
|
||||
#: packages/lib/server-only/auth/send-forgot-password.ts
|
||||
msgid "Forgot Password?"
|
||||
@@ -2907,7 +2899,7 @@ msgstr "Authentification d'action de destinataire globale"
|
||||
#: apps/remix/app/components/general/generic-error-layout.tsx
|
||||
#: packages/ui/primitives/document-flow/document-flow-root.tsx
|
||||
msgid "Go Back"
|
||||
msgstr "Retour"
|
||||
msgstr "Retourner"
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/verify-email._index.tsx
|
||||
#: apps/remix/app/routes/_unauthenticated+/verify-email.$token.tsx
|
||||
@@ -2988,7 +2980,7 @@ msgstr "Salut, je suis Timur"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx
|
||||
msgid "Hi {userName},"
|
||||
msgstr "Bonjour {userName},"
|
||||
msgstr "Bonjour, {userName},"
|
||||
|
||||
#: packages/email/templates/reset-password.tsx
|
||||
msgid "Hi, {userName} <0>({userEmail})</0>"
|
||||
@@ -3010,7 +3002,7 @@ msgstr "Je suis un signataire de ce document"
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts
|
||||
msgid "I am a viewer of this document"
|
||||
msgstr "Je suis un lecteur de ce document"
|
||||
msgstr "Je suis un visualiseur de ce document"
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts
|
||||
msgid "I am an approver of this document"
|
||||
@@ -3381,11 +3373,6 @@ msgstr "Gérer les paramètres de votre site ici"
|
||||
msgid "Manager"
|
||||
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
|
||||
msgid "Mark as Viewed"
|
||||
msgstr "Marquer comme vu"
|
||||
@@ -3400,11 +3387,11 @@ msgstr "MAU (document terminé)"
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
|
||||
msgid "Max"
|
||||
msgstr "Maximum"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
||||
msgid "Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use template defaults."
|
||||
msgstr "Taille maximale du fichier : 4 Mo. Maximum de 100 lignes par importation. Les valeurs vides utiliseront les valeurs par défaut du modèle."
|
||||
msgstr "Taille maximale du fichier : 4 Mo. Maximum de 100 lignes par téléversement. Les valeurs vides utiliseront les valeurs par défaut du modèle."
|
||||
|
||||
#: packages/lib/constants/teams.ts
|
||||
msgid "Member"
|
||||
@@ -4001,10 +3988,6 @@ 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."
|
||||
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
|
||||
msgid "Please review the document before signing."
|
||||
msgstr "Veuillez examiner le document avant de signer."
|
||||
@@ -5215,7 +5198,7 @@ msgstr "Modèle supprimé"
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-create-dialog.tsx
|
||||
msgid "Template document uploaded"
|
||||
msgstr "Document modèle importé"
|
||||
msgstr "Document modèle téléchargé"
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
|
||||
msgid "Template duplicated"
|
||||
@@ -5280,11 +5263,11 @@ msgstr "Couleur du texte"
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
|
||||
msgid "Thank you for using Documenso to perform your electronic document signing. The purpose of this disclosure is to inform you about the process, legality, and your rights regarding the use of electronic signatures on our platform. By opting to use an electronic signature, you are agreeing to the terms and conditions outlined below."
|
||||
msgstr "Merci d'utiliser Documenso pour signer vos documents électroniquement. L'objectif de cette clause est de vous informer sur le processus, la légalité et vos droits concernant l'utilisation de la signature électronique sur notre plateforme. En choisissant d'utiliser un sytème de signature électronique, vous acceptez les termes et conditions exposés ci-dessous."
|
||||
msgstr "Merci d'utiliser Documenso pour signer vos documents électroniquement. L'objectif de cette divulgation est de vous informer sur le processus, la légalité et vos droits concernant l'utilisation des signatures électroniques sur notre plateforme. En choisissant d'utiliser une signature électronique, vous acceptez les termes et conditions énoncés ci-dessous."
|
||||
|
||||
#: packages/email/template-components/template-forgot-password.tsx
|
||||
msgid "That's okay, it happens! Click the button below to reset your password."
|
||||
msgstr "Ce n'est pas grave, cela arrive ! Cliquez sur le bouton ci-dessous pour réinitialiser votre mot de passe."
|
||||
msgstr "C'est d'accord, cela arrive ! Cliquez sur le bouton ci-dessous pour réinitialiser votre mot de passe."
|
||||
|
||||
#: apps/remix/app/components/dialogs/admin-user-delete-dialog.tsx
|
||||
msgid "The account has been deleted successfully."
|
||||
@@ -5522,7 +5505,7 @@ msgstr "Le webhook a été créé avec succès."
|
||||
|
||||
#: apps/remix/app/components/tables/documents-table-empty-state.tsx
|
||||
msgid "There are no active drafts at the current moment. You can upload a document to start drafting."
|
||||
msgstr "Il n'y a pas de brouillons actifs pour le moment. Vous pouvez importer un document pour commencer un brouillon."
|
||||
msgstr "Il n'y a pas de brouillons actifs pour le moment. Vous pouvez télécharger un document pour commencer à rédiger."
|
||||
|
||||
#: apps/remix/app/components/tables/documents-table-empty-state.tsx
|
||||
msgid "There are no completed documents yet. Documents that you have created or received will appear here once completed."
|
||||
@@ -6091,27 +6074,27 @@ msgstr "Améliorer"
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
||||
msgid "Upload a CSV file to create multiple documents from this template. Each row represents one document with its recipient details."
|
||||
msgstr "Importer un fichier CSV pour créer plusieurs documents à partir de ce modèle. Chaque ligne représente un document avec les coordonnées de son destinataire."
|
||||
msgstr "Téléchargez un fichier CSV pour créer plusieurs documents à partir de ce modèle. Chaque ligne représente un document avec ses détails de destinataire."
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
|
||||
msgid "Upload a custom document to use instead of the template's default document"
|
||||
msgstr "Importer un document personnalisé à utiliser à la place du modèle par défaut"
|
||||
msgstr "Téléchargez un document personnalisé à utiliser à la place du document par défaut du modèle"
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
||||
msgid "Upload and Process"
|
||||
msgstr "Importer et traiter"
|
||||
msgstr "Télécharger et traiter"
|
||||
|
||||
#: apps/remix/app/components/forms/avatar-image.tsx
|
||||
msgid "Upload Avatar"
|
||||
msgstr "Importer un avatar"
|
||||
msgstr "Télécharger un avatar"
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
||||
msgid "Upload CSV"
|
||||
msgstr "Importer le CSV"
|
||||
msgstr "Télécharger le CSV"
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
|
||||
msgid "Upload custom document"
|
||||
msgstr "Importer un document personnalisé"
|
||||
msgstr "Télécharger un document personnalisé"
|
||||
|
||||
#: packages/ui/primitives/signature-pad/signature-pad.tsx
|
||||
msgid "Upload Signature"
|
||||
@@ -6119,7 +6102,7 @@ msgstr "Importer une signature"
|
||||
|
||||
#: packages/ui/primitives/document-dropzone.tsx
|
||||
msgid "Upload Template Document"
|
||||
msgstr "Importer le document modèle"
|
||||
msgstr "Télécharger le document modèle"
|
||||
|
||||
#: apps/remix/app/components/forms/team-branding-preferences-form.tsx
|
||||
msgid "Upload your brand logo (max 5MB, JPG, PNG, or WebP)"
|
||||
@@ -6132,15 +6115,15 @@ msgstr "Téléversé par"
|
||||
|
||||
#: apps/remix/app/components/forms/avatar-image.tsx
|
||||
msgid "Uploaded file is too large"
|
||||
msgstr "Le fichier importé est trop volumineux"
|
||||
msgstr "Le fichier téléchargé est trop volumineux"
|
||||
|
||||
#: apps/remix/app/components/forms/avatar-image.tsx
|
||||
msgid "Uploaded file is too small"
|
||||
msgstr "Le fichier importé est trop petit"
|
||||
msgstr "Le fichier téléchargé est trop petit"
|
||||
|
||||
#: apps/remix/app/components/forms/avatar-image.tsx
|
||||
msgid "Uploaded file not an allowed file type"
|
||||
msgstr "Le fichier importé n'est pas un type de fichier autorisé"
|
||||
msgstr "Le fichier téléchargé n'est pas un type de fichier autorisé"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/templates.$id._index.tsx
|
||||
msgid "Use"
|
||||
@@ -6232,7 +6215,7 @@ msgstr "Vérifiez votre adresse e-mail pour débloquer toutes les fonctionnalit
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload.tsx
|
||||
msgid "Verify your email to upload documents."
|
||||
msgstr "Vérifiez votre e-mail pour importer des documents."
|
||||
msgstr "Vérifiez votre e-mail pour télécharger des documents."
|
||||
|
||||
#: packages/email/templates/confirm-team-email.tsx
|
||||
msgid "Verify your team email address"
|
||||
@@ -6329,11 +6312,11 @@ msgstr "Vu"
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts
|
||||
msgid "Viewer"
|
||||
msgstr "Lecteur"
|
||||
msgstr "Visiteur"
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts
|
||||
msgid "Viewers"
|
||||
msgstr "Lecteurs"
|
||||
msgstr "Spectateurs"
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts
|
||||
msgid "Viewing"
|
||||
@@ -6728,7 +6711,7 @@ msgstr "Vous êtes sur le point d'envoyer ce document aux destinataires. Êtes-v
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/settings+/billing.tsx
|
||||
msgid "You are currently on the <0>Free Plan</0>."
|
||||
msgstr "Vous êtes actuellement sur l'<0>Abonnement Gratuit</0>."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/team-member-update-dialog.tsx
|
||||
msgid "You are currently updating <0>{teamMemberName}.</0>"
|
||||
@@ -6809,11 +6792,11 @@ msgstr "Vous ne pouvez pas modifier un membre de l'équipe qui a un rôle plus
|
||||
|
||||
#: packages/ui/primitives/document-dropzone.tsx
|
||||
msgid "You cannot upload documents at this time."
|
||||
msgstr "Vous ne pouvez pas importer de documents pour le moment."
|
||||
msgstr "Vous ne pouvez pas télécharger de documents pour le moment."
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload.tsx
|
||||
msgid "You cannot upload encrypted PDFs"
|
||||
msgstr "Vous ne pouvez pas importer de PDF cryptés"
|
||||
msgstr "Vous ne pouvez pas télécharger de PDF cryptés"
|
||||
|
||||
#: apps/remix/app/components/general/billing-portal-button.tsx
|
||||
msgid "You do not currently have a customer record, this should not happen. Please contact support for assistance."
|
||||
@@ -6885,11 +6868,11 @@ msgstr "Vous n'avez pas encore de webhooks. Vos webhooks seront affichés ici un
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/templates._index.tsx
|
||||
msgid "You have not yet created any templates. To create a template please upload one."
|
||||
msgstr "Vous n'avez pas encore créé de modèles. Pour créer un modèle, veuillez en importer un."
|
||||
msgstr "Vous n'avez pas encore créé de modèles. Pour créer un modèle, veuillez en télécharger un."
|
||||
|
||||
#: apps/remix/app/components/tables/documents-table-empty-state.tsx
|
||||
msgid "You have not yet created or received any documents. To create a document please upload one."
|
||||
msgstr "Vous n'avez pas encore créé ou reçu de documents. Pour créer un document, veuillez en importer un."
|
||||
msgstr "Vous n'avez pas encore créé ou reçu de documents. Pour créer un document, veuillez en télécharger un."
|
||||
|
||||
#. placeholder {0}: quota.directTemplates
|
||||
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
|
||||
@@ -6898,7 +6881,7 @@ msgstr "Vous avez atteint la limite maximale de {0} modèles directs. <0>Mettez
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload.tsx
|
||||
msgid "You have reached your document limit for this month. Please upgrade your plan."
|
||||
msgstr "Vous avez atteint votre limite de documents pour ce mois. Veuillez passer à l'abonnement supérieur."
|
||||
msgstr "Vous avez atteint votre limite de documents pour ce mois. Veuillez passer à un plan supérieur."
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload.tsx
|
||||
#: packages/ui/primitives/document-dropzone.tsx
|
||||
@@ -7024,11 +7007,11 @@ msgstr "Votre envoi groupé a été initié. Vous recevrez une notification par
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx
|
||||
msgid "Your bulk send operation for template \"{templateName}\" has completed."
|
||||
msgstr "Votre envoi groupé pour le modèle \"{templateName}\" est terminé."
|
||||
msgstr "Votre opération d'envoi groupé pour le modèle \"{templateName}\" est terminée."
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/settings+/billing.tsx
|
||||
msgid "Your current plan is past due. Please update your payment information."
|
||||
msgstr "Votre abonnement actuel est arrivé à échéance. Veuillez mettre à jour vos informations de paiement."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
|
||||
msgid "Your direct signing templates"
|
||||
@@ -7036,7 +7019,7 @@ msgstr "Vos modèles de signature directe"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload.tsx
|
||||
msgid "Your document failed to upload."
|
||||
msgstr "L'importation de votre document a échoué."
|
||||
msgstr "Votre document a échoué à se télécharger."
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
|
||||
msgid "Your document has been created from the template successfully."
|
||||
@@ -7060,11 +7043,11 @@ msgstr "Votre document a été dupliqué avec succès."
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload.tsx
|
||||
msgid "Your document has been uploaded successfully."
|
||||
msgstr "Votre document a été importé avec succès."
|
||||
msgstr "Votre document a été téléchargé avec succès."
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-create-dialog.tsx
|
||||
msgid "Your document has been uploaded successfully. You will be redirected to the template page."
|
||||
msgstr "Votre document a été importé avec succès. Vous serez redirigé vers la page de modèle."
|
||||
msgstr "Votre document a été téléchargé avec succès. Vous serez redirigé vers la page de modèle."
|
||||
|
||||
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
|
||||
msgid "Your document preferences have been updated"
|
||||
|
||||
@@ -473,7 +473,6 @@ 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?"
|
||||
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
|
||||
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?"
|
||||
@@ -1145,7 +1144,6 @@ msgstr "Versione dell'app"
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.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/document-page-view-button.tsx
|
||||
#: packages/lib/constants/recipient-roles.ts
|
||||
msgid "Approve"
|
||||
@@ -1567,7 +1565,6 @@ msgstr "Clicca per inserire il campo"
|
||||
msgid "Close"
|
||||
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/forms/signup.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
@@ -1579,10 +1576,6 @@ msgstr "Completa"
|
||||
msgid "Complete Approval"
|
||||
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
|
||||
msgid "Complete Document"
|
||||
msgstr ""
|
||||
@@ -1595,7 +1588,6 @@ msgstr "Completa la Firma"
|
||||
msgid "Complete the fields for the following signers. Once reviewed, they will inform you if any modifications are needed."
|
||||
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
|
||||
msgid "Complete Viewing"
|
||||
msgstr "Completa la Visualizzazione"
|
||||
@@ -3381,11 +3373,6 @@ msgstr "Gestisci le impostazioni del tuo sito qui"
|
||||
msgid "Manager"
|
||||
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
|
||||
msgid "Mark as Viewed"
|
||||
msgstr "Segna come visto"
|
||||
@@ -4001,10 +3988,6 @@ 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."
|
||||
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
|
||||
msgid "Please review the document before signing."
|
||||
msgstr "Rivedi il documento prima di firmare."
|
||||
|
||||
@@ -473,7 +473,6 @@ 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?"
|
||||
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
|
||||
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?"
|
||||
@@ -1145,7 +1144,6 @@ msgstr "Wersja aplikacji"
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.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/document-page-view-button.tsx
|
||||
#: packages/lib/constants/recipient-roles.ts
|
||||
msgid "Approve"
|
||||
@@ -1567,7 +1565,6 @@ msgstr "Kliknij, aby wstawić pole"
|
||||
msgid "Close"
|
||||
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/forms/signup.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
@@ -1579,10 +1576,6 @@ msgstr "Zakończono"
|
||||
msgid "Complete Approval"
|
||||
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
|
||||
msgid "Complete Document"
|
||||
msgstr ""
|
||||
@@ -1595,7 +1588,6 @@ msgstr "Zakończ podpisywanie"
|
||||
msgid "Complete the fields for the following signers. Once reviewed, they will inform you if any modifications are needed."
|
||||
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
|
||||
msgid "Complete Viewing"
|
||||
msgstr "Zakończ wyświetlanie"
|
||||
@@ -3381,11 +3373,6 @@ msgstr "Zarządzaj ustawieniami swojej witryny tutaj"
|
||||
msgid "Manager"
|
||||
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
|
||||
msgid "Mark as Viewed"
|
||||
msgstr "Oznacz jako wyświetlone"
|
||||
@@ -4001,10 +3988,6 @@ msgstr "Proszę podać token z aplikacji uwierzytelniającej lub kod zapasowy. J
|
||||
msgid "Please provide a token from your authenticator, or a backup code."
|
||||
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
|
||||
msgid "Please review the document before signing."
|
||||
msgstr "Proszę przejrzeć dokument przed podpisaniem."
|
||||
|
||||
@@ -51,7 +51,6 @@ export const ZDocumentSchema = DocumentSchema.pick({
|
||||
documentId: true,
|
||||
redirectUrl: true,
|
||||
typedSignatureEnabled: true,
|
||||
allowDictateNextSigner: true,
|
||||
language: true,
|
||||
emailSettings: true,
|
||||
}).nullable(),
|
||||
|
||||
@@ -122,44 +122,6 @@ export const ZFieldMetaNotOptionalSchema = z.discriminatedUnion('type', [
|
||||
|
||||
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
|
||||
.union([
|
||||
// Handles an empty object being provided as fieldMeta.
|
||||
|
||||
@@ -45,7 +45,6 @@ export const ZTemplateSchema = TemplateSchema.pick({
|
||||
dateFormat: true,
|
||||
signingOrder: true,
|
||||
typedSignatureEnabled: true,
|
||||
allowDictateNextSigner: true,
|
||||
distributionMethod: true,
|
||||
templateId: true,
|
||||
redirectUrl: true,
|
||||
|
||||
@@ -46,7 +46,6 @@ export const ZWebhookDocumentMetaSchema = z.object({
|
||||
dateFormat: z.string(),
|
||||
redirectUrl: z.string().nullable(),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||
allowDictateNextSigner: z.boolean(),
|
||||
typedSignatureEnabled: z.boolean(),
|
||||
language: z.string(),
|
||||
distributionMethod: z.nativeEnum(DocumentDistributionMethod),
|
||||
|
||||
@@ -40,13 +40,7 @@ export type ApiRequestMetadata = {
|
||||
};
|
||||
|
||||
export const extractRequestMetadata = (req: Request): RequestMetadata => {
|
||||
const forwardedFor = req.headers.get('x-forwarded-for');
|
||||
const ip = forwardedFor
|
||||
?.split(',')
|
||||
.map((ip) => ip.trim())
|
||||
.at(0);
|
||||
|
||||
const parsedIp = ZIpSchema.safeParse(ip);
|
||||
const parsedIp = ZIpSchema.safeParse(req.headers.get('x-forwarded-for'));
|
||||
|
||||
const ipAddress = parsedIp.success ? parsedIp.data : undefined;
|
||||
const userAgent = req.headers.get('user-agent');
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import type { Document } from '@prisma/client';
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
|
||||
export const isDocumentCompleted = (document: Pick<Document, 'status'> | DocumentStatus) => {
|
||||
const status = typeof document === 'string' ? document : document.status;
|
||||
|
||||
return status === DocumentStatus.COMPLETED || status === DocumentStatus.REJECTED;
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "DocumentStatus" ADD VALUE 'REJECTED';
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "DocumentMeta" ADD COLUMN "allowDictateNextSigner" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "TemplateMeta" ADD COLUMN "allowDictateNextSigner" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -297,7 +297,6 @@ enum DocumentStatus {
|
||||
DRAFT
|
||||
PENDING
|
||||
COMPLETED
|
||||
REJECTED
|
||||
}
|
||||
|
||||
enum DocumentSource {
|
||||
@@ -390,21 +389,20 @@ enum DocumentDistributionMethod {
|
||||
|
||||
/// @zod.import(["import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';"])
|
||||
model DocumentMeta {
|
||||
id String @id @default(cuid())
|
||||
subject String?
|
||||
message String?
|
||||
timezone String? @default("Etc/UTC") @db.Text
|
||||
password String?
|
||||
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
|
||||
documentId Int @unique
|
||||
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
redirectUrl String?
|
||||
signingOrder DocumentSigningOrder @default(PARALLEL)
|
||||
allowDictateNextSigner Boolean @default(false)
|
||||
typedSignatureEnabled Boolean @default(true)
|
||||
language String @default("en")
|
||||
distributionMethod DocumentDistributionMethod @default(EMAIL)
|
||||
emailSettings Json? /// [DocumentEmailSettings] @zod.custom.use(ZDocumentEmailSettingsSchema)
|
||||
id String @id @default(cuid())
|
||||
subject String?
|
||||
message String?
|
||||
timezone String? @default("Etc/UTC") @db.Text
|
||||
password String?
|
||||
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
|
||||
documentId Int @unique
|
||||
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
redirectUrl String?
|
||||
signingOrder DocumentSigningOrder @default(PARALLEL)
|
||||
typedSignatureEnabled Boolean @default(true)
|
||||
language String @default("en")
|
||||
distributionMethod DocumentDistributionMethod @default(EMAIL)
|
||||
emailSettings Json? /// [DocumentEmailSettings] @zod.custom.use(ZDocumentEmailSettingsSchema)
|
||||
}
|
||||
|
||||
enum ReadStatus {
|
||||
@@ -661,16 +659,15 @@ enum TemplateType {
|
||||
|
||||
/// @zod.import(["import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';"])
|
||||
model TemplateMeta {
|
||||
id String @id @default(cuid())
|
||||
subject String?
|
||||
message String?
|
||||
timezone String? @default("Etc/UTC") @db.Text
|
||||
password String?
|
||||
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
|
||||
signingOrder DocumentSigningOrder? @default(PARALLEL)
|
||||
allowDictateNextSigner Boolean @default(false)
|
||||
typedSignatureEnabled Boolean @default(true)
|
||||
distributionMethod DocumentDistributionMethod @default(EMAIL)
|
||||
id String @id @default(cuid())
|
||||
subject String?
|
||||
message String?
|
||||
timezone String? @default("Etc/UTC") @db.Text
|
||||
password String?
|
||||
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
|
||||
signingOrder DocumentSigningOrder? @default(PARALLEL)
|
||||
typedSignatureEnabled Boolean @default(true)
|
||||
distributionMethod DocumentDistributionMethod @default(EMAIL)
|
||||
|
||||
templateId Int @unique
|
||||
template Template @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@ -31,7 +31,6 @@ type DocumentToSeed = {
|
||||
|
||||
export const seedDocuments = async (documents: DocumentToSeed[]) => {
|
||||
await Promise.all(
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
documents.map(async (document, i) =>
|
||||
match(document.type)
|
||||
.with(DocumentStatus.DRAFT, async () =>
|
||||
@@ -51,7 +50,8 @@ export const seedDocuments = async (documents: DocumentToSeed[]) => {
|
||||
key: i,
|
||||
createDocumentOptions: document.documentOptions,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.exhaustive(),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { findDocuments } from '@documenso/lib/server-only/admin/get-all-documents';
|
||||
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
|
||||
@@ -11,7 +13,6 @@ import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
|
||||
import { disableUser } from '@documenso/lib/server-only/user/disable-user';
|
||||
import { enableUser } from '@documenso/lib/server-only/user/enable-user';
|
||||
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
|
||||
import { adminProcedure, router } from '../trpc';
|
||||
import {
|
||||
@@ -69,7 +70,7 @@ export const adminRouter = router({
|
||||
|
||||
const document = await getEntireDocument({ id });
|
||||
|
||||
const isResealing = isDocumentCompleted(document.status);
|
||||
const isResealing = document.status === DocumentStatus.COMPLETED;
|
||||
|
||||
return await sealDocument({ documentId: id, isResealing });
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DocumentDataType } from '@prisma/client';
|
||||
import { DocumentDataType, DocumentStatus } from '@prisma/client';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
@@ -26,7 +26,6 @@ import { sendDocument } from '@documenso/lib/server-only/document/send-document'
|
||||
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
||||
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
|
||||
import { authenticatedProcedure, procedure, router } from '../trpc';
|
||||
import {
|
||||
@@ -371,7 +370,6 @@ export const documentRouter = router({
|
||||
redirectUrl: meta.redirectUrl,
|
||||
distributionMethod: meta.distributionMethod,
|
||||
signingOrder: meta.signingOrder,
|
||||
allowDictateNextSigner: meta.allowDictateNextSigner,
|
||||
emailSettings: meta.emailSettings,
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
@@ -661,7 +659,7 @@ export const documentRouter = router({
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (!isDocumentCompleted(document.status)) {
|
||||
if (document.status !== DocumentStatus.COMPLETED) {
|
||||
throw new AppError('DOCUMENT_NOT_COMPLETE');
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user