Compare commits

..

31 Commits

Author SHA1 Message Date
5880e903ec fix: remove visual regression images until new alignment updates are in 2025-11-07 00:21:27 +11:00
d8b91fcf9a fix: test 2025-11-06 23:57:24 +11:00
7fc68a0a94 chore: resolve build errors 2025-11-06 23:49:47 +11:00
c40471281a chore: update embeds for v2 envelopes 2025-11-06 23:45:06 +11:00
f72cabf5ca fix: remove content-length 2025-11-06 22:20:59 +11:00
4e38d861f6 fix: move open meta around 2025-11-06 16:40:55 +11:00
1592fbd369 fix: field hover 2025-11-06 15:43:36 +11:00
77c4d1d26d fix: test 2025-11-06 10:30:31 +11:00
16b7b71ef4 fix: test 2025-11-06 10:24:10 +11:00
36b9a14563 fix: test 2025-11-05 22:29:37 +11:00
db2f912a08 fix: update create envelope item endpoint to use formdata 2025-11-05 22:10:17 +11:00
fc2e9af6a0 fix: add preview page 2025-11-05 17:18:15 +11:00
a810d20a4f chore: update package lock 2025-11-05 16:42:42 +11:00
22011fd4ba fix: finish file stuff 2025-11-05 14:51:07 +11:00
717fa8f870 fix: add endpoints for getting files 2025-11-04 15:18:11 +11:00
8663c8f883 fix: various envelope updates 2025-11-04 14:57:42 +11:00
c89ca83f44 fix: redirect v2 beta url 2025-11-04 11:55:07 +11:00
bbf1dd3c6b fix: add tests 2025-11-03 20:30:35 +11:00
c10c95ca00 fix: add tests 2025-11-03 20:17:52 +11:00
4a0425b120 feat: add formdata endpoints for documents,envelopes,templates
Adds the missing endpoints for documents, envelopes and
templates supporting file uploads in a singular request.

Also updates frontend components that would use the prior
hidden endpoints.
2025-11-03 15:11:20 +11:00
a6e923dd8a feat: allow multipart requests for public api
Adds support for multipart/form-data requests in the public api
allowing documents to be uploaded without having to perform a secondary
request.

Need to rollout further endpoints for envelopes and templates.

Need to change how we store files to not use `putFileServerSide`
2025-11-03 15:10:28 +11:00
7e38d06ef5 Merge branch 'main' into feat/add-envelopes-api 2025-11-01 12:47:55 +11:00
4e2443396c fix: increase res 2025-10-31 20:49:57 +11:00
2e2980f04f fix: increase res 2025-10-31 20:28:45 +11:00
3efe0de52f fix: increase threshold 2025-10-31 17:43:33 +11:00
efbd133f0e fix: increase threshold 2025-10-31 17:21:33 +11:00
4993e8a306 fix: test 2025-10-31 17:06:59 +11:00
f93d34c38e fix: clean up endpoints 2025-10-31 15:48:05 +11:00
8c228f965a fix: test 2025-10-31 15:06:20 +11:00
9020bbc753 fix: add regression test 2025-10-31 12:38:14 +11:00
f6bdb34b56 feat: add envelopes api 2025-10-28 20:32:24 +11:00
121 changed files with 4225 additions and 7763 deletions

View File

@ -336,7 +336,7 @@ export const EnvelopeDistributeDialog = ({
<Trans>Message</Trans>{' '} <Trans>Message</Trans>{' '}
<span className="text-muted-foreground">(Optional)</span> <span className="text-muted-foreground">(Optional)</span>
<Tooltip> <Tooltip>
<TooltipTrigger type="button"> <TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" /> <InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="text-muted-foreground p-4"> <TooltipContent className="text-muted-foreground p-4">

View File

@ -185,10 +185,6 @@ export const OrganisationMemberInviteDialog = ({
return 'form'; return 'form';
} }
if (fullOrganisation.members.length < fullOrganisation.organisationClaim.memberCount) {
return 'form';
}
// This is probably going to screw us over in the future. // This is probably going to screw us over in the future.
if (fullOrganisation.organisationClaim.originalSubscriptionClaimId !== INTERNAL_CLAIM_ID.TEAM) { if (fullOrganisation.organisationClaim.originalSubscriptionClaimId !== INTERNAL_CLAIM_ID.TEAM) {
return 'alert'; return 'alert';

View File

@ -1,4 +1,5 @@
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { createCallable } from 'react-call'; import { createCallable } from 'react-call';
@ -27,71 +28,49 @@ import {
} from '@documenso/ui/primitives/form/form'; } from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
const createNumberFieldSchema = (fieldMeta: TNumberFieldMeta) => {
let schema = z.coerce.number({
invalid_type_error: msg`Please enter a valid number`.id,
});
const { numberFormat, minValue, maxValue } = fieldMeta;
if (typeof minValue === 'number') {
schema = schema.min(minValue);
}
if (typeof maxValue === 'number') {
schema = schema.max(maxValue);
}
if (numberFormat) {
const foundRegex = numberFormatValues.find((item) => item.value === numberFormat)?.regex;
if (!foundRegex) {
return schema;
}
return schema.refine(
(value) => {
return foundRegex.test(value.toString());
},
{
message: msg`Number needs to be formatted as ${numberFormat}`.id,
},
);
}
return schema;
};
export type SignFieldNumberDialogProps = { export type SignFieldNumberDialogProps = {
fieldMeta: TNumberFieldMeta; fieldMeta: TNumberFieldMeta;
}; };
export const SignFieldNumberDialog = createCallable<SignFieldNumberDialogProps, string | null>( export const SignFieldNumberDialog = createCallable<SignFieldNumberDialogProps, number | null>(
({ call, fieldMeta }) => { ({ call, fieldMeta }) => {
const { t } = useLingui(); const { t } = useLingui();
// Needs to be inside dialog for translation purposes.
const createNumberFieldSchema = (fieldMeta: TNumberFieldMeta) => {
const { numberFormat, minValue, maxValue } = fieldMeta;
if (numberFormat) {
const foundRegex = numberFormatValues.find((item) => item.value === numberFormat)?.regex;
if (foundRegex) {
return z.string().refine(
(value) => {
return foundRegex.test(value.toString());
},
{
message: t`Number needs to be formatted as ${numberFormat}`,
},
);
}
}
// Not gong to work with min/max numbers + number format
// Since currently doesn't work in V1 going to ignore for now.
return z.string().superRefine((value, ctx) => {
const isValidNumber = /^[0-9,.]+$/.test(value.toString());
if (!isValidNumber) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t`Please enter a valid number`,
});
return;
}
if (typeof minValue === 'number' && parseFloat(value) < minValue) {
ctx.addIssue({
code: z.ZodIssueCode.too_small,
minimum: minValue,
inclusive: true,
type: 'number',
});
return;
}
if (typeof maxValue === 'number' && parseFloat(value) > maxValue) {
ctx.addIssue({
code: z.ZodIssueCode.too_big,
maximum: maxValue,
inclusive: true,
type: 'number',
});
return;
}
});
};
const ZSignFieldNumberFormSchema = z.object({ const ZSignFieldNumberFormSchema = z.object({
number: createNumberFieldSchema(fieldMeta), number: createNumberFieldSchema(fieldMeta),
}); });

View File

@ -9,7 +9,6 @@ export type EmbedAuthenticationRequiredProps = {
email?: string; email?: string;
returnTo: string; returnTo: string;
isGoogleSSOEnabled?: boolean; isGoogleSSOEnabled?: boolean;
isMicrosoftSSOEnabled?: boolean;
isOIDCSSOEnabled?: boolean; isOIDCSSOEnabled?: boolean;
oidcProviderLabel?: string; oidcProviderLabel?: string;
}; };
@ -18,7 +17,6 @@ export const EmbedAuthenticationRequired = ({
email, email,
returnTo, returnTo,
// isGoogleSSOEnabled, // isGoogleSSOEnabled,
// isMicrosoftSSOEnabled,
// isOIDCSSOEnabled, // isOIDCSSOEnabled,
// oidcProviderLabel, // oidcProviderLabel,
}: EmbedAuthenticationRequiredProps) => { }: EmbedAuthenticationRequiredProps) => {
@ -39,7 +37,6 @@ export const EmbedAuthenticationRequired = ({
<SignInForm <SignInForm
// Embed currently not supported. // Embed currently not supported.
// isGoogleSSOEnabled={isGoogleSSOEnabled} // isGoogleSSOEnabled={isGoogleSSOEnabled}
// isMicrosoftSSOEnabled={isMicrosoftSSOEnabled}
// isOIDCSSOEnabled={isOIDCSSOEnabled} // isOIDCSSOEnabled={isOIDCSSOEnabled}
// oidcProviderLabel={oidcProviderLabel} // oidcProviderLabel={oidcProviderLabel}
className="mt-4" className="mt-4"

View File

@ -336,7 +336,7 @@ export const EmbedDirectTemplateClientPage = ({
<div className="flex-1"> <div className="flex-1">
<PDFViewer <PDFViewer
envelopeItem={envelopeItems[0]} envelopeItem={envelopeItems[0]}
token={recipient.token} token={token}
version="signed" version="signed"
onDocumentLoad={() => setHasDocumentLoaded(true)} onDocumentLoad={() => setHasDocumentLoaded(true)}
/> />

View File

@ -7,7 +7,6 @@ import type { z } from 'zod';
import { import {
DEFAULT_FIELD_FONT_SIZE, DEFAULT_FIELD_FONT_SIZE,
type TDateFieldMeta as DateFieldMeta, type TDateFieldMeta as DateFieldMeta,
FIELD_DEFAULT_GENERIC_ALIGN,
ZDateFieldMeta, ZDateFieldMeta,
} from '@documenso/lib/types/field-meta'; } from '@documenso/lib/types/field-meta';
import { Form } from '@documenso/ui/primitives/form/form'; import { Form } from '@documenso/ui/primitives/form/form';
@ -40,7 +39,7 @@ export const EditorFieldDateForm = ({
mode: 'onChange', mode: 'onChange',
defaultValues: { defaultValues: {
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE, fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN, textAlign: value.textAlign || 'left',
}, },
}); });

View File

@ -7,7 +7,6 @@ import type { z } from 'zod';
import { import {
DEFAULT_FIELD_FONT_SIZE, DEFAULT_FIELD_FONT_SIZE,
type TEmailFieldMeta as EmailFieldMeta, type TEmailFieldMeta as EmailFieldMeta,
FIELD_DEFAULT_GENERIC_ALIGN,
ZEmailFieldMeta, ZEmailFieldMeta,
} from '@documenso/lib/types/field-meta'; } from '@documenso/lib/types/field-meta';
import { Form } from '@documenso/ui/primitives/form/form'; import { Form } from '@documenso/ui/primitives/form/form';
@ -40,7 +39,7 @@ export const EditorFieldEmailForm = ({
mode: 'onChange', mode: 'onChange',
defaultValues: { defaultValues: {
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE, fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN, textAlign: value.textAlign || 'left',
}, },
}); });

View File

@ -3,10 +3,6 @@ import { useEffect } from 'react';
import { Trans, useLingui } from '@lingui/react/macro'; import { Trans, useLingui } from '@lingui/react/macro';
import { type Control, useFormContext } from 'react-hook-form'; import { type Control, useFormContext } from 'react-hook-form';
import { FIELD_MIN_LINE_HEIGHT } from '@documenso/lib/types/field-meta';
import { FIELD_MAX_LINE_HEIGHT } from '@documenso/lib/types/field-meta';
import { FIELD_MIN_LETTER_SPACING } from '@documenso/lib/types/field-meta';
import { FIELD_MAX_LETTER_SPACING } from '@documenso/lib/types/field-meta';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Checkbox } from '@documenso/ui/primitives/checkbox'; import { Checkbox } from '@documenso/ui/primitives/checkbox';
import { import {
@ -111,119 +107,6 @@ export const EditorGenericTextAlignField = ({
); );
}; };
export const EditorGenericVerticalAlignField = ({
formControl,
className,
}: {
formControl: FormControlType;
className?: string;
}) => {
const { t } = useLingui();
return (
<FormField
control={formControl}
name="verticalAlign"
render={({ field }) => (
<FormItem className={className}>
<FormLabel>
<Trans>Vertical Align</Trans>
</FormLabel>
<FormControl>
<Select {...field} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder={t`Select vertical align`} />
</SelectTrigger>
<SelectContent>
<SelectItem value="top">
<Trans>Top</Trans>
</SelectItem>
<SelectItem value="middle">
<Trans>Middle</Trans>
</SelectItem>
<SelectItem value="bottom">
<Trans>Bottom</Trans>
</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
};
export const EditorGenericLineHeightField = ({
formControl,
className,
}: {
formControl: FormControlType;
className?: string;
}) => {
const { t } = useLingui();
return (
<FormField
control={formControl}
name="lineHeight"
render={({ field }) => (
<FormItem className={className}>
<FormLabel>
<Trans>Line Height</Trans>
</FormLabel>
<FormControl>
<Input
type="number"
min={FIELD_MIN_LINE_HEIGHT}
max={FIELD_MAX_LINE_HEIGHT}
className="bg-background"
placeholder={t`Line height`}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
};
export const EditorGenericLetterSpacingField = ({
formControl,
className,
}: {
formControl: FormControlType;
className?: string;
}) => {
const { t } = useLingui();
return (
<FormField
control={formControl}
name="letterSpacing"
render={({ field }) => (
<FormItem className={className}>
<FormLabel>
<Trans>Letter Spacing</Trans>
</FormLabel>
<FormControl>
<Input
type="number"
min={FIELD_MIN_LETTER_SPACING}
max={FIELD_MAX_LETTER_SPACING}
className="bg-background"
placeholder={t`Letter spacing`}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
};
export const EditorGenericRequiredField = ({ export const EditorGenericRequiredField = ({
formControl, formControl,
className, className,

View File

@ -6,7 +6,6 @@ import type { z } from 'zod';
import { import {
DEFAULT_FIELD_FONT_SIZE, DEFAULT_FIELD_FONT_SIZE,
FIELD_DEFAULT_GENERIC_ALIGN,
type TInitialsFieldMeta as InitialsFieldMeta, type TInitialsFieldMeta as InitialsFieldMeta,
ZInitialsFieldMeta, ZInitialsFieldMeta,
} from '@documenso/lib/types/field-meta'; } from '@documenso/lib/types/field-meta';
@ -40,7 +39,7 @@ export const EditorFieldInitialsForm = ({
mode: 'onChange', mode: 'onChange',
defaultValues: { defaultValues: {
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE, fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN, textAlign: value.textAlign || 'left',
}, },
}); });

View File

@ -6,7 +6,6 @@ import type { z } from 'zod';
import { import {
DEFAULT_FIELD_FONT_SIZE, DEFAULT_FIELD_FONT_SIZE,
FIELD_DEFAULT_GENERIC_ALIGN,
type TNameFieldMeta as NameFieldMeta, type TNameFieldMeta as NameFieldMeta,
ZNameFieldMeta, ZNameFieldMeta,
} from '@documenso/lib/types/field-meta'; } from '@documenso/lib/types/field-meta';
@ -40,7 +39,7 @@ export const EditorFieldNameForm = ({
mode: 'onChange', mode: 'onChange',
defaultValues: { defaultValues: {
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE, fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN, textAlign: value.textAlign || 'left',
}, },
}); });

View File

@ -6,11 +6,6 @@ import { useForm, useWatch } from 'react-hook-form';
import type { z } from 'zod'; import type { z } from 'zod';
import { import {
DEFAULT_FIELD_FONT_SIZE,
FIELD_DEFAULT_GENERIC_ALIGN,
FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN,
FIELD_DEFAULT_LETTER_SPACING,
FIELD_DEFAULT_LINE_HEIGHT,
type TNumberFieldMeta as NumberFieldMeta, type TNumberFieldMeta as NumberFieldMeta,
ZNumberFieldMeta, ZNumberFieldMeta,
} from '@documenso/lib/types/field-meta'; } from '@documenso/lib/types/field-meta';
@ -36,12 +31,9 @@ import { Separator } from '@documenso/ui/primitives/separator';
import { import {
EditorGenericFontSizeField, EditorGenericFontSizeField,
EditorGenericLabelField, EditorGenericLabelField,
EditorGenericLetterSpacingField,
EditorGenericLineHeightField,
EditorGenericReadOnlyField, EditorGenericReadOnlyField,
EditorGenericRequiredField, EditorGenericRequiredField,
EditorGenericTextAlignField, EditorGenericTextAlignField,
EditorGenericVerticalAlignField,
} from './editor-field-generic-field-forms'; } from './editor-field-generic-field-forms';
const ZNumberFieldFormSchema = ZNumberFieldMeta.pick({ const ZNumberFieldFormSchema = ZNumberFieldMeta.pick({
@ -51,9 +43,6 @@ const ZNumberFieldFormSchema = ZNumberFieldMeta.pick({
numberFormat: true, numberFormat: true,
fontSize: true, fontSize: true,
textAlign: true, textAlign: true,
lineHeight: true,
letterSpacing: true,
verticalAlign: true,
required: true, required: true,
readOnly: true, readOnly: true,
minValue: true, minValue: true,
@ -110,11 +99,8 @@ export const EditorFieldNumberForm = ({
placeholder: value.placeholder || '', placeholder: value.placeholder || '',
value: value.value || '', value: value.value || '',
numberFormat: value.numberFormat || null, numberFormat: value.numberFormat || null,
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE, fontSize: value.fontSize || 14,
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN, textAlign: value.textAlign || 'left',
lineHeight: value.lineHeight ?? FIELD_DEFAULT_LINE_HEIGHT,
letterSpacing: value.letterSpacing ?? FIELD_DEFAULT_LETTER_SPACING,
verticalAlign: value.verticalAlign ?? FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN,
required: value.required || false, required: value.required || false,
readOnly: value.readOnly || false, readOnly: value.readOnly || false,
minValue: value.minValue, minValue: value.minValue,
@ -132,10 +118,6 @@ export const EditorFieldNumberForm = ({
useEffect(() => { useEffect(() => {
const validatedFormValues = ZNumberFieldFormSchema.safeParse(formValues); const validatedFormValues = ZNumberFieldFormSchema.safeParse(formValues);
if (formValues.readOnly && !formValues.value) {
void form.trigger('value');
}
if (validatedFormValues.success) { if (validatedFormValues.success) {
onValueChange({ onValueChange({
type: 'number', type: 'number',
@ -148,12 +130,10 @@ export const EditorFieldNumberForm = ({
<Form {...form}> <Form {...form}>
<form> <form>
<fieldset className="flex flex-col gap-2"> <fieldset className="flex flex-col gap-2">
<EditorGenericFontSizeField className="w-full" formControl={form.control} />
<div className="flex w-full flex-row gap-x-4"> <div className="flex w-full flex-row gap-x-4">
<EditorGenericTextAlignField className="w-full" formControl={form.control} /> <EditorGenericFontSizeField className="w-full" formControl={form.control} />
<EditorGenericVerticalAlignField className="w-full" formControl={form.control} /> <EditorGenericTextAlignField className="w-full" formControl={form.control} />
</div> </div>
<EditorGenericLabelField formControl={form.control} /> <EditorGenericLabelField formControl={form.control} />
@ -224,12 +204,6 @@ export const EditorFieldNumberForm = ({
)} )}
/> />
<div className="flex w-full flex-row gap-x-4">
<EditorGenericLineHeightField className="w-full" formControl={form.control} />
<EditorGenericLetterSpacingField className="w-full" formControl={form.control} />
</div>
<div className="mt-1"> <div className="mt-1">
<EditorGenericRequiredField formControl={form.control} /> <EditorGenericRequiredField formControl={form.control} />
</div> </div>

View File

@ -5,8 +5,11 @@ import { Trans } from '@lingui/react/macro';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import type { z } from 'zod'; import type { z } from 'zod';
import { DEFAULT_SIGNATURE_TEXT_FONT_SIZE } from '@documenso/lib/constants/pdf'; import {
import { type TSignatureFieldMeta, ZSignatureFieldMeta } from '@documenso/lib/types/field-meta'; DEFAULT_FIELD_FONT_SIZE,
type TSignatureFieldMeta,
ZSignatureFieldMeta,
} from '@documenso/lib/types/field-meta';
import { Form } from '@documenso/ui/primitives/form/form'; import { Form } from '@documenso/ui/primitives/form/form';
import { EditorGenericFontSizeField } from './editor-field-generic-field-forms'; import { EditorGenericFontSizeField } from './editor-field-generic-field-forms';
@ -32,7 +35,7 @@ export const EditorFieldSignatureForm = ({
resolver: zodResolver(ZSignatureFieldFormSchema), resolver: zodResolver(ZSignatureFieldFormSchema),
mode: 'onChange', mode: 'onChange',
defaultValues: { defaultValues: {
fontSize: value.fontSize || DEFAULT_SIGNATURE_TEXT_FONT_SIZE, fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
}, },
}); });

View File

@ -3,16 +3,11 @@ import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro'; import { Trans, useLingui } from '@lingui/react/macro';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import type { z } from 'zod'; import { z } from 'zod';
import { import {
DEFAULT_FIELD_FONT_SIZE, DEFAULT_FIELD_FONT_SIZE,
FIELD_DEFAULT_GENERIC_ALIGN,
FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN,
FIELD_DEFAULT_LETTER_SPACING,
FIELD_DEFAULT_LINE_HEIGHT,
type TTextFieldMeta as TextFieldMeta, type TTextFieldMeta as TextFieldMeta,
ZTextFieldMeta,
} from '@documenso/lib/types/field-meta'; } from '@documenso/lib/types/field-meta';
import { import {
Form, Form,
@ -27,36 +22,32 @@ import { Textarea } from '@documenso/ui/primitives/textarea';
import { import {
EditorGenericFontSizeField, EditorGenericFontSizeField,
EditorGenericLetterSpacingField,
EditorGenericLineHeightField,
EditorGenericReadOnlyField, EditorGenericReadOnlyField,
EditorGenericRequiredField, EditorGenericRequiredField,
EditorGenericTextAlignField, EditorGenericTextAlignField,
EditorGenericVerticalAlignField,
} from './editor-field-generic-field-forms'; } from './editor-field-generic-field-forms';
const ZTextFieldFormSchema = ZTextFieldMeta.pick({ const ZTextFieldFormSchema = z
label: true, .object({
placeholder: true, label: z.string().optional(),
text: true, placeholder: z.string().optional(),
characterLimit: true, text: z.string().optional(),
fontSize: true, characterLimit: z.coerce.number().min(0).optional(),
textAlign: true, fontSize: z.coerce.number().min(8).max(96).optional(),
lineHeight: true, textAlign: z.enum(['left', 'center', 'right']).optional(),
letterSpacing: true, required: z.boolean().optional(),
verticalAlign: true, readOnly: z.boolean().optional(),
required: true, })
readOnly: true, .refine(
}).refine( (data) => {
(data) => { // A read-only field must have text
// A read-only field must have text return !data.readOnly || (data.text && data.text.length > 0);
return !data.readOnly || (data.text && data.text.length > 0); },
}, {
{ message: 'A read-only field must have text',
message: 'A read-only field must have text', path: ['text'],
path: ['text'], },
}, );
);
type TTextFieldFormSchema = z.infer<typeof ZTextFieldFormSchema>; type TTextFieldFormSchema = z.infer<typeof ZTextFieldFormSchema>;
@ -82,10 +73,7 @@ export const EditorFieldTextForm = ({
text: value.text || '', text: value.text || '',
characterLimit: value.characterLimit || 0, characterLimit: value.characterLimit || 0,
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE, fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN, textAlign: value.textAlign || 'left',
lineHeight: value.lineHeight ?? FIELD_DEFAULT_LINE_HEIGHT,
letterSpacing: value.letterSpacing ?? FIELD_DEFAULT_LETTER_SPACING,
verticalAlign: value.verticalAlign ?? FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN,
required: value.required || false, required: value.required || false,
readOnly: value.readOnly || false, readOnly: value.readOnly || false,
}, },
@ -101,10 +89,6 @@ export const EditorFieldTextForm = ({
useEffect(() => { useEffect(() => {
const validatedFormValues = ZTextFieldFormSchema.safeParse(formValues); const validatedFormValues = ZTextFieldFormSchema.safeParse(formValues);
if (formValues.readOnly && !formValues.text) {
void form.trigger('text');
}
if (validatedFormValues.success) { if (validatedFormValues.success) {
onValueChange({ onValueChange({
type: 'text', type: 'text',
@ -117,12 +101,10 @@ export const EditorFieldTextForm = ({
<Form {...form}> <Form {...form}>
<form> <form>
<fieldset className="flex flex-col gap-2"> <fieldset className="flex flex-col gap-2">
<EditorGenericFontSizeField className="w-full" formControl={form.control} />
<div className="flex w-full flex-row gap-x-4"> <div className="flex w-full flex-row gap-x-4">
<EditorGenericTextAlignField className="w-full" formControl={form.control} /> <EditorGenericFontSizeField className="w-full" formControl={form.control} />
<EditorGenericVerticalAlignField className="w-full" formControl={form.control} /> <EditorGenericTextAlignField className="w-full" formControl={form.control} />
</div> </div>
<FormField <FormField
@ -200,16 +182,17 @@ export const EditorFieldTextForm = ({
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input
type="number"
min={0}
className="bg-background" className="bg-background"
placeholder={t`Character limit`} placeholder={t`Field character limit`}
{...field} {...field}
value={field.value || ''}
onChange={(e) => { onChange={(e) => {
field.onChange(e);
const values = form.getValues(); const values = form.getValues();
const characterLimit = parseInt(e.target.value, 10) || 0; const characterLimit = parseInt(e.target.value, 10) || 0;
field.onChange(characterLimit || '');
const textValue = values.text || ''; const textValue = values.text || '';
if (characterLimit > 0 && textValue.length > characterLimit) { if (characterLimit > 0 && textValue.length > characterLimit) {
@ -223,12 +206,6 @@ export const EditorFieldTextForm = ({
)} )}
/> />
<div className="flex w-full flex-row gap-x-4">
<EditorGenericLineHeightField className="w-full" formControl={form.control} />
<EditorGenericLetterSpacingField className="w-full" formControl={form.control} />
</div>
<div className="mt-1"> <div className="mt-1">
<EditorGenericRequiredField formControl={form.control} /> <EditorGenericRequiredField formControl={form.control} />
</div> </div>

View File

@ -92,7 +92,6 @@ export const SignInForm = ({
const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] = const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] =
useState(false); useState(false);
const [isEmbeddedRedirect, setIsEmbeddedRedirect] = useState(false);
const [twoFactorAuthenticationMethod, setTwoFactorAuthenticationMethod] = useState< const [twoFactorAuthenticationMethod, setTwoFactorAuthenticationMethod] = useState<
'totp' | 'backup' 'totp' | 'backup'
@ -318,8 +317,6 @@ export const SignInForm = ({
if (email) { if (email) {
form.setValue('email', email); form.setValue('email', email);
} }
setIsEmbeddedRedirect(params.get('embedded') === 'true');
}, [form]); }, [form]);
return ( return (
@ -386,64 +383,56 @@ export const SignInForm = ({
{isSubmitting ? <Trans>Signing in...</Trans> : <Trans>Sign In</Trans>} {isSubmitting ? <Trans>Signing in...</Trans> : <Trans>Sign In</Trans>}
</Button> </Button>
{!isEmbeddedRedirect && ( {hasSocialAuthEnabled && (
<> <div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
{hasSocialAuthEnabled && ( <div className="bg-border h-px flex-1" />
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase"> <span className="text-muted-foreground bg-transparent">
<div className="bg-border h-px flex-1" /> <Trans>Or continue with</Trans>
<span className="text-muted-foreground bg-transparent"> </span>
<Trans>Or continue with</Trans> <div className="bg-border h-px flex-1" />
</span> </div>
<div className="bg-border h-px flex-1" /> )}
</div>
)}
{isGoogleSSOEnabled && ( {isGoogleSSOEnabled && (
<Button <Button
type="button" type="button"
size="lg" size="lg"
variant="outline" variant="outline"
className="bg-background text-muted-foreground border" className="bg-background text-muted-foreground border"
disabled={isSubmitting} disabled={isSubmitting}
onClick={onSignInWithGoogleClick} onClick={onSignInWithGoogleClick}
> >
<FcGoogle className="mr-2 h-5 w-5" /> <FcGoogle className="mr-2 h-5 w-5" />
Google Google
</Button> </Button>
)} )}
{isMicrosoftSSOEnabled && ( {isMicrosoftSSOEnabled && (
<Button <Button
type="button" type="button"
size="lg" size="lg"
variant="outline" variant="outline"
className="bg-background text-muted-foreground border" className="bg-background text-muted-foreground border"
disabled={isSubmitting} disabled={isSubmitting}
onClick={onSignInWithMicrosoftClick} onClick={onSignInWithMicrosoftClick}
> >
<img <img className="mr-2 h-4 w-4" alt="Microsoft Logo" src={'/static/microsoft.svg'} />
className="mr-2 h-4 w-4" Microsoft
alt="Microsoft Logo" </Button>
src={'/static/microsoft.svg'} )}
/>
Microsoft
</Button>
)}
{isOIDCSSOEnabled && ( {isOIDCSSOEnabled && (
<Button <Button
type="button" type="button"
size="lg" size="lg"
variant="outline" variant="outline"
className="bg-background text-muted-foreground border" className="bg-background text-muted-foreground border"
disabled={isSubmitting} disabled={isSubmitting}
onClick={onSignInWithOIDCClick} onClick={onSignInWithOIDCClick}
> >
<FaIdCardClip className="mr-2 h-5 w-5" /> <FaIdCardClip className="mr-2 h-5 w-5" />
{oidcProviderLabel || 'OIDC'} {oidcProviderLabel || 'OIDC'}
</Button> </Button>
)}
</>
)} )}
<Button <Button

View File

@ -68,7 +68,6 @@ export type SignUpFormProps = {
isGoogleSSOEnabled?: boolean; isGoogleSSOEnabled?: boolean;
isMicrosoftSSOEnabled?: boolean; isMicrosoftSSOEnabled?: boolean;
isOIDCSSOEnabled?: boolean; isOIDCSSOEnabled?: boolean;
returnTo?: string;
}; };
export const SignUpForm = ({ export const SignUpForm = ({
@ -77,7 +76,6 @@ export const SignUpForm = ({
isGoogleSSOEnabled, isGoogleSSOEnabled,
isMicrosoftSSOEnabled, isMicrosoftSSOEnabled,
isOIDCSSOEnabled, isOIDCSSOEnabled,
returnTo,
}: SignUpFormProps) => { }: SignUpFormProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
@ -112,7 +110,7 @@ export const SignUpForm = ({
signature, signature,
}); });
await navigate(returnTo ? returnTo : '/unverified-account'); await navigate(`/unverified-account`);
toast({ toast({
title: _(msg`Registration Successful`), title: _(msg`Registration Successful`),

View File

@ -9,7 +9,6 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { Theme, useTheme } from 'remix-themes'; import { Theme, useTheme } from 'remix-themes';
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
import { useSession } from '@documenso/lib/client-only/providers/session'; import { useSession } from '@documenso/lib/client-only/providers/session';
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n'; import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { import {
@ -64,12 +63,10 @@ export function AppCommandMenu({ open, onOpenChange }: AppCommandMenuProps) {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [pages, setPages] = useState<string[]>([]); const [pages, setPages] = useState<string[]>([]);
const debouncedSearch = useDebouncedValue(search, 200);
const { data: searchDocumentsData, isPending: isSearchingDocuments } = const { data: searchDocumentsData, isPending: isSearchingDocuments } =
trpcReact.document.search.useQuery( trpcReact.document.search.useQuery(
{ {
query: debouncedSearch, query: search,
}, },
{ {
placeholderData: (previousData) => previousData, placeholderData: (previousData) => previousData,
@ -235,7 +232,6 @@ export function AppCommandMenu({ open, onOpenChange }: AppCommandMenuProps) {
<Trans>No results found.</Trans> <Trans>No results found.</Trans>
</CommandEmpty> </CommandEmpty>
)} )}
{!currentPage && ( {!currentPage && (
<> <>
{documentPageLinks.length > 0 && ( {documentPageLinks.length > 0 && (
@ -243,17 +239,14 @@ export function AppCommandMenu({ open, onOpenChange }: AppCommandMenuProps) {
<Commands push={push} pages={documentPageLinks} /> <Commands push={push} pages={documentPageLinks} />
</CommandGroup> </CommandGroup>
)} )}
{templatePageLinks.length > 0 && ( {templatePageLinks.length > 0 && (
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Templates`)}> <CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Templates`)}>
<Commands push={push} pages={templatePageLinks} /> <Commands push={push} pages={templatePageLinks} />
</CommandGroup> </CommandGroup>
)} )}
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Settings`)}> <CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Settings`)}>
<Commands push={push} pages={SETTINGS_PAGES} /> <Commands push={push} pages={SETTINGS_PAGES} />
</CommandGroup> </CommandGroup>
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Preferences`)}> <CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Preferences`)}>
<CommandItem className="-mx-2 -my-1 rounded-lg" onSelect={() => addPage('language')}> <CommandItem className="-mx-2 -my-1 rounded-lg" onSelect={() => addPage('language')}>
Change language Change language
@ -262,7 +255,6 @@ export function AppCommandMenu({ open, onOpenChange }: AppCommandMenuProps) {
Change theme Change theme
</CommandItem> </CommandItem>
</CommandGroup> </CommandGroup>
{searchResults.length > 0 && ( {searchResults.length > 0 && (
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Your documents`)}> <CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Your documents`)}>
<Commands push={push} pages={searchResults} /> <Commands push={push} pages={searchResults} />

View File

@ -22,7 +22,7 @@ export const DocumentSigningAuthAccount = ({
actionVerb = 'sign', actionVerb = 'sign',
onOpenChange, onOpenChange,
}: DocumentSigningAuthAccountProps) => { }: DocumentSigningAuthAccountProps) => {
const { recipient, isDirectTemplate } = useRequiredDocumentSigningAuthContext(); const { recipient } = useRequiredDocumentSigningAuthContext();
const { t } = useLingui(); const { t } = useLingui();
@ -34,10 +34,8 @@ export const DocumentSigningAuthAccount = ({
try { try {
setIsSigningOut(true); setIsSigningOut(true);
const currentPath = `${window.location.pathname}${window.location.search}${window.location.hash}`;
await authClient.signOut({ await authClient.signOut({
redirectPath: `/signin?returnTo=${encodeURIComponent(currentPath)}#embedded=true&email=${isDirectTemplate ? '' : email}`, redirectPath: `/signin#email=${email}`,
}); });
} catch { } catch {
setIsSigningOut(false); setIsSigningOut(false);
@ -57,28 +55,16 @@ export const DocumentSigningAuthAccount = ({
<AlertDescription> <AlertDescription>
{actionTarget === 'DOCUMENT' && recipient.role === RecipientRole.VIEWER ? ( {actionTarget === 'DOCUMENT' && recipient.role === RecipientRole.VIEWER ? (
<span> <span>
{isDirectTemplate ? ( <Trans>
<Trans>To mark this document as viewed, you need to be logged in.</Trans> To mark this document as viewed, you need to be logged in as{' '}
) : ( <strong>{recipient.email}</strong>
<Trans> </Trans>
To mark this document as viewed, you need to be logged in as{' '}
<strong>{recipient.email}</strong>
</Trans>
)}
</span> </span>
) : ( ) : (
<span> <span>
{isDirectTemplate ? ( {/* Todo: Translate */}
<Trans> To {actionVerb.toLowerCase()} this {actionTarget.toLowerCase()}, you need to be logged
To {actionVerb.toLowerCase()} this {actionTarget.toLowerCase()}, you need to be in as <strong>{recipient.email}</strong>
logged in.
</Trans>
) : (
<Trans>
To {actionVerb.toLowerCase()} this {actionTarget.toLowerCase()}, you need to be
logged in as <strong>{recipient.email}</strong>
</Trans>
)}
</span> </span>
)} )}
</AlertDescription> </AlertDescription>

View File

@ -47,8 +47,7 @@ export const DocumentSigningAuthDialog = ({
onOpenChange, onOpenChange,
onReauthFormSubmit, onReauthFormSubmit,
}: DocumentSigningAuthDialogProps) => { }: DocumentSigningAuthDialogProps) => {
const { recipient, user, isCurrentlyAuthenticating, isDirectTemplate } = const { recipient, user, isCurrentlyAuthenticating } = useRequiredDocumentSigningAuthContext();
useRequiredDocumentSigningAuthContext();
// Filter out EXPLICIT_NONE from available auth types for the chooser // Filter out EXPLICIT_NONE from available auth types for the chooser
const validAuthTypes = availableAuthTypes.filter( const validAuthTypes = availableAuthTypes.filter(
@ -169,11 +168,7 @@ export const DocumentSigningAuthDialog = ({
match({ documentAuthType: selectedAuthType, user }) match({ documentAuthType: selectedAuthType, user })
.with( .with(
{ documentAuthType: DocumentAuth.ACCOUNT }, { documentAuthType: DocumentAuth.ACCOUNT },
{ { user: P.when((user) => !user || user.email !== recipient.email) }, // Assume all current auth methods requires them to be logged in.
user: P.when(
(user) => !user || (user.email !== recipient.email && !isDirectTemplate),
),
}, // Assume all current auth methods requires them to be logged in.
() => <DocumentSigningAuthAccount onOpenChange={onOpenChange} />, () => <DocumentSigningAuthAccount onOpenChange={onOpenChange} />,
) )
.with({ documentAuthType: DocumentAuth.PASSKEY }, () => ( .with({ documentAuthType: DocumentAuth.PASSKEY }, () => (

View File

@ -40,7 +40,6 @@ export type DocumentSigningAuthContextValue = {
derivedRecipientAccessAuth: TRecipientAccessAuthTypes[]; derivedRecipientAccessAuth: TRecipientAccessAuthTypes[];
derivedRecipientActionAuth: TRecipientActionAuthTypes[]; derivedRecipientActionAuth: TRecipientActionAuthTypes[];
isAuthRedirectRequired: boolean; isAuthRedirectRequired: boolean;
isDirectTemplate?: boolean;
isCurrentlyAuthenticating: boolean; isCurrentlyAuthenticating: boolean;
setIsCurrentlyAuthenticating: (_value: boolean) => void; setIsCurrentlyAuthenticating: (_value: boolean) => void;
passkeyData: PasskeyData; passkeyData: PasskeyData;
@ -69,7 +68,6 @@ export const useRequiredDocumentSigningAuthContext = () => {
export interface DocumentSigningAuthProviderProps { export interface DocumentSigningAuthProviderProps {
documentAuthOptions: Envelope['authOptions']; documentAuthOptions: Envelope['authOptions'];
recipient: SigningAuthRecipient; recipient: SigningAuthRecipient;
isDirectTemplate?: boolean;
user?: SessionUser | null; user?: SessionUser | null;
children: React.ReactNode; children: React.ReactNode;
} }
@ -77,7 +75,6 @@ export interface DocumentSigningAuthProviderProps {
export const DocumentSigningAuthProvider = ({ export const DocumentSigningAuthProvider = ({
documentAuthOptions: initialDocumentAuthOptions, documentAuthOptions: initialDocumentAuthOptions,
recipient: initialRecipient, recipient: initialRecipient,
isDirectTemplate = false,
user, user,
children, children,
}: DocumentSigningAuthProviderProps) => { }: DocumentSigningAuthProviderProps) => {
@ -207,7 +204,6 @@ export const DocumentSigningAuthProvider = ({
derivedRecipientAccessAuth, derivedRecipientAccessAuth,
derivedRecipientActionAuth, derivedRecipientActionAuth,
isAuthRedirectRequired, isAuthRedirectRequired,
isDirectTemplate,
isCurrentlyAuthenticating, isCurrentlyAuthenticating,
setIsCurrentlyAuthenticating, setIsCurrentlyAuthenticating,
passkeyData, passkeyData,

View File

@ -184,10 +184,10 @@ export const DocumentSigningPageViewV2 = () => {
</div> </div>
)} )}
<div className="embed--DocumentWidgetFooter mt-auto"> <div className="embed--DocumentWidgetFooter">
{/* Footer of left sidebar. */} {/* Footer of left sidebar. */}
{!isEmbed && ( {!isEmbed && (
<div className="px-4"> <div className="mt-auto px-4">
<Button asChild variant="ghost" className="w-full justify-start"> <Button asChild variant="ghost" className="w-full justify-start">
<Link to="/"> <Link to="/">
<ArrowLeftIcon className="mr-2 h-4 w-4" /> <ArrowLeftIcon className="mr-2 h-4 w-4" />

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { type DocumentData, DocumentStatus, type EnvelopeItem, EnvelopeType } from '@prisma/client'; import { type DocumentData, DocumentStatus, type EnvelopeItem } from '@prisma/client';
import { DownloadIcon } from 'lucide-react'; import { DownloadIcon } from 'lucide-react';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
@ -100,14 +100,7 @@ export const DocumentCertificateQRView = ({
)} )}
{internalVersion === 2 ? ( {internalVersion === 2 ? (
<EnvelopeRenderProvider <EnvelopeRenderProvider envelope={{ envelopeItems }} token={token}>
envelope={{
envelopeItems,
status: DocumentStatus.COMPLETED,
type: EnvelopeType.DOCUMENT,
}}
token={token}
>
<DocumentCertificateQrV2 <DocumentCertificateQrV2
title={title} title={title}
recipientCount={recipientCount} recipientCount={recipientCount}
@ -137,7 +130,7 @@ export const DocumentCertificateQRView = ({
envelopeItems={envelopeItems} envelopeItems={envelopeItems}
token={token} token={token}
trigger={ trigger={
<Button type="button" variant="outline" className="w-fit"> <Button type="button" variant="outline" className="flex-1">
<DownloadIcon className="mr-2 h-5 w-5" /> <DownloadIcon className="mr-2 h-5 w-5" />
<Trans>Download</Trans> <Trans>Download</Trans>
</Button> </Button>
@ -196,7 +189,7 @@ const DocumentCertificateQrV2 = ({
envelopeItems={envelopeItems} envelopeItems={envelopeItems}
token={token} token={token}
trigger={ trigger={
<Button type="button" variant="outline" className="w-fit"> <Button type="button" variant="outline" className="flex-1">
<DownloadIcon className="mr-2 h-5 w-5" /> <DownloadIcon className="mr-2 h-5 w-5" />
<Trans>Download</Trans> <Trans>Download</Trans>
</Button> </Button>

View File

@ -7,7 +7,6 @@ import { DateTime } from 'luxon';
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted'; import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
import type { TEnvelope } from '@documenso/lib/types/envelope'; import type { TEnvelope } from '@documenso/lib/types/envelope';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
export type DocumentPageViewInformationProps = { export type DocumentPageViewInformationProps = {
userId: number; userId: number;
@ -41,10 +40,6 @@ export const DocumentPageViewInformation = ({
.setLocale(i18n.locales?.[0] || i18n.locale) .setLocale(i18n.locales?.[0] || i18n.locale)
.toRelative(), .toRelative(),
}, },
{
description: msg`Document ID (Legacy)`,
value: mapSecondaryIdToDocumentId(envelope.secondaryId),
},
]; ];
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMounted, envelope, userId]); }, [isMounted, envelope, userId]);

View File

@ -616,14 +616,13 @@ export default function EnvelopeEditorFieldsPageRenderer() {
transform: 'translateX(-50%)', transform: 'translateX(-50%)',
zIndex: 50, zIndex: 50,
}} }}
// Don't use darkmode for this component, it should look the same for both light/dark modes. className="text-muted-foreground grid w-max grid-cols-5 gap-x-1 gap-y-0.5 rounded-md border bg-white p-1 shadow-sm"
className="grid w-max grid-cols-5 gap-x-1 gap-y-0.5 rounded-md border border-gray-300 bg-white p-1 text-gray-500 shadow-sm"
> >
{fieldButtonList.map((field) => ( {fieldButtonList.map((field) => (
<button <button
key={field.type} key={field.type}
onClick={() => createFieldFromPendingTemplate(pendingFieldCreation, field.type)} onClick={() => createFieldFromPendingTemplate(pendingFieldCreation, field.type)}
className="col-span-1 w-full flex-shrink-0 rounded-sm px-2 py-1 text-xs hover:bg-gray-100 hover:text-gray-600" className="hover:text-foreground col-span-1 w-full flex-shrink-0 rounded-sm px-2 py-1 text-xs hover:bg-gray-100"
> >
{t(field.name)} {t(field.name)}
</button> </button>

View File

@ -2,7 +2,7 @@ import { lazy, useEffect, useMemo, useState } from 'react';
import { faker } from '@faker-js/faker/locale/en'; import { faker } from '@faker-js/faker/locale/en';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { FieldType, SigningStatus } from '@prisma/client'; import { FieldType } from '@prisma/client';
import { FileTextIcon } from 'lucide-react'; import { FileTextIcon } from 'lucide-react';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
@ -201,10 +201,7 @@ export const EnvelopeEditorPreviewPage = () => {
envelope={envelope} envelope={envelope}
token={undefined} token={undefined}
fields={fieldsWithPlaceholders} fields={fieldsWithPlaceholders}
recipients={envelope.recipients.map((recipient) => ({ recipients={envelope.recipients}
...recipient,
signingStatus: SigningStatus.SIGNED,
}))}
overrideSettings={{ overrideSettings={{
mode: 'export', mode: 'export',
}} }}

View File

@ -212,7 +212,7 @@ export const EnvelopeEditorRecipientForm = () => {
); );
const hasDocumentBeenSent = recipients.some( const hasDocumentBeenSent = recipients.some(
(recipient) => recipient.role !== RecipientRole.CC && recipient.sendStatus === SendStatus.SENT, (recipient) => recipient.sendStatus === SendStatus.SENT,
); );
const canRecipientBeModified = (recipientId?: number) => { const canRecipientBeModified = (recipientId?: number) => {

View File

@ -49,7 +49,7 @@ export const EnvelopeEditorUploadPage = () => {
const organisation = useCurrentOrganisation(); const organisation = useCurrentOrganisation();
const { t } = useLingui(); const { t } = useLingui();
const { envelope, setLocalEnvelope, relativePath, editorFields } = useCurrentEnvelopeEditor(); const { envelope, setLocalEnvelope, relativePath } = useCurrentEnvelopeEditor();
const { maximumEnvelopeItemCount, remaining } = useLimits(); const { maximumEnvelopeItemCount, remaining } = useLimits();
const { toast } = useToast(); const { toast } = useToast();
@ -165,17 +165,9 @@ export const EnvelopeEditorUploadPage = () => {
const onFileDelete = (envelopeItemId: string) => { const onFileDelete = (envelopeItemId: string) => {
setLocalFiles((prev) => prev.filter((uploadingFile) => uploadingFile.id !== envelopeItemId)); setLocalFiles((prev) => prev.filter((uploadingFile) => uploadingFile.id !== envelopeItemId));
const fieldsWithoutDeletedItem = envelope.fields.filter(
(field) => field.envelopeItemId !== envelopeItemId,
);
setLocalEnvelope({ setLocalEnvelope({
envelopeItems: envelope.envelopeItems.filter((item) => item.id !== envelopeItemId), envelopeItems: envelope.envelopeItems.filter((item) => item.id !== envelopeItemId),
fields: envelope.fields.filter((field) => field.envelopeItemId !== envelopeItemId),
}); });
// Reset editor fields.
editorFields.resetForm(fieldsWithoutDeletedItem);
}; };
/** /**

View File

@ -1,7 +1,7 @@
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { DocumentStatus, type Recipient, SigningStatus } from '@prisma/client'; import { type Recipient, SigningStatus } from '@prisma/client';
import type Konva from 'konva'; import type Konva from 'konva';
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer'; import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
@ -19,7 +19,6 @@ export default function EnvelopeGenericPageRenderer() {
const { i18n } = useLingui(); const { i18n } = useLingui();
const { const {
envelopeStatus,
currentEnvelopeItem, currentEnvelopeItem,
fields, fields,
recipients, recipients,
@ -43,10 +42,6 @@ export default function EnvelopeGenericPageRenderer() {
const { _className, scale } = pageContext; const { _className, scale } = pageContext;
const localPageFields = useMemo((): GenericLocalField[] => { const localPageFields = useMemo((): GenericLocalField[] => {
if (envelopeStatus === DocumentStatus.COMPLETED) {
return [];
}
return fields return fields
.filter( .filter(
(field) => (field) =>
@ -59,20 +54,11 @@ export default function EnvelopeGenericPageRenderer() {
throw new Error(`Recipient not found for field ${field.id}`); throw new Error(`Recipient not found for field ${field.id}`);
} }
const isInserted = recipient.signingStatus === SigningStatus.SIGNED && field.inserted;
return { return {
...field, ...field,
inserted: isInserted,
customText: isInserted ? field.customText : '',
recipient, recipient,
}; };
}) });
.filter(
({ inserted, fieldMeta, recipient }) =>
(recipient.signingStatus === SigningStatus.SIGNED ? inserted : true) ||
fieldMeta?.readOnly,
);
}, [fields, pageContext.pageNumber, currentEnvelopeItem?.id, recipients]); }, [fields, pageContext.pageNumber, currentEnvelopeItem?.id, recipients]);
const unsafeRenderFieldOnLayer = (field: GenericLocalField) => { const unsafeRenderFieldOnLayer = (field: GenericLocalField) => {
@ -81,8 +67,12 @@ export default function EnvelopeGenericPageRenderer() {
return; return;
} }
const { recipient } = field;
const fieldTranslations = getClientSideFieldTranslations(i18n); const fieldTranslations = getClientSideFieldTranslations(i18n);
const isInserted = recipient.signingStatus === SigningStatus.SIGNED && field.inserted;
renderField({ renderField({
scale, scale,
pageLayer: pageLayer.current, pageLayer: pageLayer.current,
@ -93,6 +83,7 @@ export default function EnvelopeGenericPageRenderer() {
height: Number(field.height), height: Number(field.height),
positionX: Number(field.positionX), positionX: Number(field.positionX),
positionY: Number(field.positionY), positionY: Number(field.positionY),
customText: isInserted ? field.customText : '',
fieldMeta: field.fieldMeta, fieldMeta: field.fieldMeta,
signature: { signature: {
signatureImageAsBase64: '', signatureImageAsBase64: '',
@ -104,7 +95,7 @@ export default function EnvelopeGenericPageRenderer() {
pageHeight: unscaledViewport.height, pageHeight: unscaledViewport.height,
color: getRecipientColorKey(field.recipientId), color: getRecipientColorKey(field.recipientId),
editable: false, editable: false,
mode: overrideSettings?.mode ?? 'edit', mode: overrideSettings?.mode ?? 'sign',
}); });
}; };

View File

@ -1,14 +1,7 @@
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { Trans, useLingui } from '@lingui/react/macro'; import { Trans, useLingui } from '@lingui/react/macro';
import { import { type Field, FieldType, RecipientRole, type Signature } from '@prisma/client';
type Field,
FieldType,
type Recipient,
RecipientRole,
type Signature,
SigningStatus,
} from '@prisma/client';
import type Konva from 'konva'; import type Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node'; import type { KonvaEventObject } from 'konva/lib/Node';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
@ -19,7 +12,6 @@ import { useOptionalSession } from '@documenso/lib/client-only/providers/session
import { DIRECT_TEMPLATE_RECIPIENT_EMAIL } from '@documenso/lib/constants/direct-templates'; import { DIRECT_TEMPLATE_RECIPIENT_EMAIL } from '@documenso/lib/constants/direct-templates';
import { isBase64Image } from '@documenso/lib/constants/signatures'; import { isBase64Image } from '@documenso/lib/constants/signatures';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import type { TEnvelope } from '@documenso/lib/types/envelope';
import { ZFullFieldSchema } from '@documenso/lib/types/field'; import { ZFullFieldSchema } from '@documenso/lib/types/field';
import { createSpinner } from '@documenso/lib/universal/field-renderer/field-generic-items'; import { createSpinner } from '@documenso/lib/universal/field-renderer/field-generic-items';
import { renderField } from '@documenso/lib/universal/field-renderer/render-field'; import { renderField } from '@documenso/lib/universal/field-renderer/render-field';
@ -27,7 +19,6 @@ import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields
import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields'; import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import type { TSignEnvelopeFieldValue } from '@documenso/trpc/server/envelope-router/sign-envelope-field.types'; import type { TSignEnvelopeFieldValue } from '@documenso/trpc/server/envelope-router/sign-envelope-field.types';
import { EnvelopeRecipientFieldTooltip } from '@documenso/ui/components/document/envelope-recipient-field-tooltip';
import { EnvelopeFieldToolTip } from '@documenso/ui/components/field/envelope-field-tooltip'; import { EnvelopeFieldToolTip } from '@documenso/ui/components/field/envelope-field-tooltip';
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors'; import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
@ -45,10 +36,6 @@ import { handleTextFieldClick } from '~/utils/field-signing/text-field';
import { useRequiredDocumentSigningAuthContext } from '../document-signing/document-signing-auth-provider'; import { useRequiredDocumentSigningAuthContext } from '../document-signing/document-signing-auth-provider';
import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider'; import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider';
type GenericLocalField = TEnvelope['fields'][number] & {
recipient: Pick<Recipient, 'id' | 'name' | 'email' | 'signingStatus'>;
};
export default function EnvelopeSignerPageRenderer() { export default function EnvelopeSignerPageRenderer() {
const { t, i18n } = useLingui(); const { t, i18n } = useLingui();
const { currentEnvelopeItem, setRenderError } = useCurrentEnvelopeRender(); const { currentEnvelopeItem, setRenderError } = useCurrentEnvelopeRender();
@ -104,36 +91,6 @@ export default function EnvelopeSignerPageRenderer() {
); );
}, [recipientFields, selectedAssistantRecipientFields, pageContext.pageNumber]); }, [recipientFields, selectedAssistantRecipientFields, pageContext.pageNumber]);
/**
* Returns fields that have been fully signed by other recipients for this specific
* page.
*/
const localPageOtherRecipientFields = useMemo((): GenericLocalField[] => {
const signedRecipients = envelope.recipients.filter(
(recipient) => recipient.signingStatus === SigningStatus.SIGNED,
);
return signedRecipients.flatMap((recipient) => {
return recipient.fields
.filter(
(field) =>
field.page === pageContext.pageNumber &&
field.envelopeItemId === currentEnvelopeItem?.id &&
(field.inserted || field.fieldMeta?.readOnly),
)
.map((field) => ({
...field,
recipient: {
id: recipient.id,
name: recipient.name,
email: recipient.email,
signingStatus: recipient.signingStatus,
role: recipient.role,
},
}));
});
}, [envelope.recipients, pageContext.pageNumber]);
const unsafeRenderFieldOnLayer = (unparsedField: Field & { signature?: Signature | null }) => { const unsafeRenderFieldOnLayer = (unparsedField: Field & { signature?: Signature | null }) => {
if (!pageLayer.current) { if (!pageLayer.current) {
console.error('Layer not loaded yet'); console.error('Layer not loaded yet');
@ -419,46 +376,6 @@ export default function EnvelopeSignerPageRenderer() {
} }
}; };
const renderFields = () => {
if (!pageLayer.current) {
console.error('Layer not loaded yet');
return;
}
// Render current recipient fields.
for (const field of localPageFields) {
renderFieldOnLayer(field);
}
// Render other recipient signed and inserted fields.
for (const field of localPageOtherRecipientFields) {
try {
renderField({
scale,
pageLayer: pageLayer.current,
field: {
renderId: field.id.toString(),
...field,
width: Number(field.width),
height: Number(field.height),
positionX: Number(field.positionX),
positionY: Number(field.positionY),
fieldMeta: field.fieldMeta,
},
translations: getClientSideFieldTranslations(i18n),
pageWidth: unscaledViewport.width,
pageHeight: unscaledViewport.height,
color: 'readOnly',
editable: false,
mode: 'sign',
});
} catch (err) {
console.error('Unable to render one or more fields belonging to other recipients.');
console.error(err);
}
}
};
const signField = async ( const signField = async (
fieldId: number, fieldId: number,
payload: TSignEnvelopeFieldValue, payload: TSignEnvelopeFieldValue,
@ -495,7 +412,11 @@ export default function EnvelopeSignerPageRenderer() {
* Initialize the Konva page canvas and all fields and interactions. * Initialize the Konva page canvas and all fields and interactions.
*/ */
const createPageCanvas = (currentStage: Konva.Stage, currentPageLayer: Konva.Layer) => { const createPageCanvas = (currentStage: Konva.Stage, currentPageLayer: Konva.Layer) => {
renderFields(); // Render the fields.
for (const field of localPageFields) {
renderFieldOnLayer(field);
}
currentPageLayer.batchDraw(); currentPageLayer.batchDraw();
}; };
@ -507,7 +428,9 @@ export default function EnvelopeSignerPageRenderer() {
return; return;
} }
renderFields(); localPageFields.forEach((field) => {
renderFieldOnLayer(field);
});
pageLayer.current.batchDraw(); pageLayer.current.batchDraw();
}, [localPageFields, showPendingFieldTooltip, fullName, signature, email]); }, [localPageFields, showPendingFieldTooltip, fullName, signature, email]);
@ -523,7 +446,9 @@ export default function EnvelopeSignerPageRenderer() {
// Rerender the whole page. // Rerender the whole page.
pageLayer.current.destroyChildren(); pageLayer.current.destroyChildren();
renderFields(); localPageFields.forEach((field) => {
renderFieldOnLayer(field);
});
pageLayer.current.batchDraw(); pageLayer.current.batchDraw();
}, [selectedAssistantRecipient]); }, [selectedAssistantRecipient]);
@ -550,15 +475,6 @@ export default function EnvelopeSignerPageRenderer() {
</EnvelopeFieldToolTip> </EnvelopeFieldToolTip>
)} )}
{localPageOtherRecipientFields.map((field) => (
<EnvelopeRecipientFieldTooltip
key={field.id}
field={field}
showFieldStatus={true}
showRecipientTooltip={true}
/>
))}
{/* The element Konva will inject it's canvas into. */} {/* The element Konva will inject it's canvas into. */}
<div className="konva-container absolute inset-0 z-10 w-full" ref={konvaContainer}></div> <div className="konva-container absolute inset-0 z-10 w-full" ref={konvaContainer}></div>

View File

@ -190,7 +190,7 @@ export const EnvelopeSignerCompleteDialog = () => {
console.log('err', err); console.log('err', err);
toast({ toast({
title: t`Something went wrong`, title: t`Something went wrong`,
description: t`We were unable to submit this document at this time. Please try again later.`, description: t`Weeeeeeee were unable to submit this document at this time. Please try again later.`,
variant: 'destructive', variant: 'destructive',
}); });

View File

@ -7,13 +7,11 @@ import type { User } from '@prisma/client';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted'; import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
export type TemplatePageViewInformationProps = { export type TemplatePageViewInformationProps = {
userId: number; userId: number;
template: { template: {
userId: number; userId: number;
secondaryId: string;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
user: Pick<User, 'id' | 'name' | 'email'>; user: Pick<User, 'id' | 'name' | 'email'>;
@ -45,10 +43,6 @@ export const TemplatePageViewInformation = ({
.setLocale(i18n.locales?.[0] || i18n.locale) .setLocale(i18n.locales?.[0] || i18n.locale)
.toRelative(), .toRelative(),
}, },
{
description: msg`Template ID (Legacy)`,
value: mapSecondaryIdToTemplateId(template.secondaryId),
},
]; ];
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMounted, template, userId]); }, [isMounted, template, userId]);

View File

@ -184,7 +184,6 @@ const DirectSigningPageV1 = ({ data }: { data: Awaited<ReturnType<typeof handleV
<DocumentSigningAuthProvider <DocumentSigningAuthProvider
documentAuthOptions={template.authOptions} documentAuthOptions={template.authOptions}
recipient={directTemplateRecipient} recipient={directTemplateRecipient}
isDirectTemplate={true}
user={user} user={user}
> >
<> <>

View File

@ -1,5 +1,3 @@
import { useEffect, useState } from 'react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { Link, redirect } from 'react-router'; import { Link, redirect } from 'react-router';
@ -11,7 +9,6 @@ import {
OIDC_PROVIDER_LABEL, OIDC_PROVIDER_LABEL,
} from '@documenso/lib/constants/auth'; } from '@documenso/lib/constants/auth';
import { env } from '@documenso/lib/utils/env'; import { env } from '@documenso/lib/utils/env';
import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
import { SignInForm } from '~/components/forms/signin'; import { SignInForm } from '~/components/forms/signin';
import { appMetaTags } from '~/utils/meta'; import { appMetaTags } from '~/utils/meta';
@ -31,12 +28,8 @@ export async function loader({ request }: Route.LoaderArgs) {
const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED; const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
const oidcProviderLabel = OIDC_PROVIDER_LABEL; const oidcProviderLabel = OIDC_PROVIDER_LABEL;
let returnTo = new URL(request.url).searchParams.get('returnTo') ?? undefined;
returnTo = isValidReturnTo(returnTo) ? normalizeReturnTo(returnTo) : undefined;
if (isAuthenticated) { if (isAuthenticated) {
throw redirect(returnTo || '/'); throw redirect('/');
} }
return { return {
@ -44,28 +37,12 @@ export async function loader({ request }: Route.LoaderArgs) {
isMicrosoftSSOEnabled, isMicrosoftSSOEnabled,
isOIDCSSOEnabled, isOIDCSSOEnabled,
oidcProviderLabel, oidcProviderLabel,
returnTo,
}; };
} }
export default function SignIn({ loaderData }: Route.ComponentProps) { export default function SignIn({ loaderData }: Route.ComponentProps) {
const { const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, oidcProviderLabel } =
isGoogleSSOEnabled, loaderData;
isMicrosoftSSOEnabled,
isOIDCSSOEnabled,
oidcProviderLabel,
returnTo,
} = loaderData;
const [isEmbeddedRedirect, setIsEmbeddedRedirect] = useState(false);
useEffect(() => {
const hash = window.location.hash.slice(1);
const params = new URLSearchParams(hash);
setIsEmbeddedRedirect(params.get('embedded') === 'true');
}, []);
return ( return (
<div className="w-screen max-w-lg px-4"> <div className="w-screen max-w-lg px-4">
@ -84,17 +61,13 @@ export default function SignIn({ loaderData }: Route.ComponentProps) {
isMicrosoftSSOEnabled={isMicrosoftSSOEnabled} isMicrosoftSSOEnabled={isMicrosoftSSOEnabled}
isOIDCSSOEnabled={isOIDCSSOEnabled} isOIDCSSOEnabled={isOIDCSSOEnabled}
oidcProviderLabel={oidcProviderLabel} oidcProviderLabel={oidcProviderLabel}
returnTo={returnTo}
/> />
{!isEmbeddedRedirect && env('NEXT_PUBLIC_DISABLE_SIGNUP') !== 'true' && ( {env('NEXT_PUBLIC_DISABLE_SIGNUP') !== 'true' && (
<p className="text-muted-foreground mt-6 text-center text-sm"> <p className="text-muted-foreground mt-6 text-center text-sm">
<Trans> <Trans>
Don't have an account?{' '} Don't have an account?{' '}
<Link <Link to="/signup" className="text-documenso-700 duration-200 hover:opacity-70">
to={returnTo ? `/signup?returnTo=${encodeURIComponent(returnTo)}` : '/signup'}
className="text-documenso-700 duration-200 hover:opacity-70"
>
Sign up Sign up
</Link> </Link>
</Trans> </Trans>

View File

@ -6,7 +6,6 @@ import {
IS_OIDC_SSO_ENABLED, IS_OIDC_SSO_ENABLED,
} from '@documenso/lib/constants/auth'; } from '@documenso/lib/constants/auth';
import { env } from '@documenso/lib/utils/env'; import { env } from '@documenso/lib/utils/env';
import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
import { SignUpForm } from '~/components/forms/signup'; import { SignUpForm } from '~/components/forms/signup';
import { appMetaTags } from '~/utils/meta'; import { appMetaTags } from '~/utils/meta';
@ -17,7 +16,7 @@ export function meta() {
return appMetaTags('Sign Up'); return appMetaTags('Sign Up');
} }
export function loader({ request }: Route.LoaderArgs) { export function loader() {
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP'); const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
// SSR env variables. // SSR env variables.
@ -29,20 +28,15 @@ export function loader({ request }: Route.LoaderArgs) {
throw redirect('/signin'); throw redirect('/signin');
} }
let returnTo = new URL(request.url).searchParams.get('returnTo') ?? undefined;
returnTo = isValidReturnTo(returnTo) ? normalizeReturnTo(returnTo) : undefined;
return { return {
isGoogleSSOEnabled, isGoogleSSOEnabled,
isMicrosoftSSOEnabled, isMicrosoftSSOEnabled,
isOIDCSSOEnabled, isOIDCSSOEnabled,
returnTo,
}; };
} }
export default function SignUp({ loaderData }: Route.ComponentProps) { export default function SignUp({ loaderData }: Route.ComponentProps) {
const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, returnTo } = loaderData; const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled } = loaderData;
return ( return (
<SignUpForm <SignUpForm
@ -50,7 +44,6 @@ export default function SignUp({ loaderData }: Route.ComponentProps) {
isGoogleSSOEnabled={isGoogleSSOEnabled} isGoogleSSOEnabled={isGoogleSSOEnabled}
isMicrosoftSSOEnabled={isMicrosoftSSOEnabled} isMicrosoftSSOEnabled={isMicrosoftSSOEnabled}
isOIDCSSOEnabled={isOIDCSSOEnabled} isOIDCSSOEnabled={isOIDCSSOEnabled}
returnTo={returnTo}
/> />
); );
} }

View File

@ -2,7 +2,6 @@ import { Outlet, isRouteErrorResponse, useRouteError } from 'react-router';
import { import {
IS_GOOGLE_SSO_ENABLED, IS_GOOGLE_SSO_ENABLED,
IS_MICROSOFT_SSO_ENABLED,
IS_OIDC_SSO_ENABLED, IS_OIDC_SSO_ENABLED,
OIDC_PROVIDER_LABEL, OIDC_PROVIDER_LABEL,
} from '@documenso/lib/constants/auth'; } from '@documenso/lib/constants/auth';
@ -32,13 +31,11 @@ export function headers({ loaderHeaders }: Route.HeadersArgs) {
export function loader() { export function loader() {
// SSR env variables. // SSR env variables.
const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED; const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED;
const isMicrosoftSSOEnabled = IS_MICROSOFT_SSO_ENABLED;
const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED; const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
const oidcProviderLabel = OIDC_PROVIDER_LABEL; const oidcProviderLabel = OIDC_PROVIDER_LABEL;
return { return {
isGoogleSSOEnabled, isGoogleSSOEnabled,
isMicrosoftSSOEnabled,
isOIDCSSOEnabled, isOIDCSSOEnabled,
oidcProviderLabel, oidcProviderLabel,
}; };
@ -49,8 +46,7 @@ export default function Layout() {
} }
export function ErrorBoundary({ loaderData }: Route.ErrorBoundaryProps) { export function ErrorBoundary({ loaderData }: Route.ErrorBoundaryProps) {
const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, oidcProviderLabel } = const { isGoogleSSOEnabled, isOIDCSSOEnabled, oidcProviderLabel } = loaderData || {};
loaderData || {};
const error = useRouteError(); const error = useRouteError();
@ -61,7 +57,6 @@ export function ErrorBoundary({ loaderData }: Route.ErrorBoundaryProps) {
return ( return (
<EmbedAuthenticationRequired <EmbedAuthenticationRequired
isGoogleSSOEnabled={isGoogleSSOEnabled} isGoogleSSOEnabled={isGoogleSSOEnabled}
isMicrosoftSSOEnabled={isMicrosoftSSOEnabled}
isOIDCSSOEnabled={isOIDCSSOEnabled} isOIDCSSOEnabled={isOIDCSSOEnabled}
oidcProviderLabel={oidcProviderLabel} oidcProviderLabel={oidcProviderLabel}
email={error.data.email} email={error.data.email}

View File

@ -76,6 +76,7 @@ async function handleV1Loader({ params, request }: Route.LoaderArgs) {
throw data( throw data(
{ {
type: 'embed-authentication-required', type: 'embed-authentication-required',
email: user?.email,
returnTo: `/embed/direct/${token}`, returnTo: `/embed/direct/${token}`,
}, },
{ {
@ -318,7 +319,6 @@ const EmbedDirectTemplatePageV2 = ({
documentAuthOptions={envelope.authOptions} documentAuthOptions={envelope.authOptions}
recipient={recipient} recipient={recipient}
user={user} user={user}
isDirectTemplate={true}
> >
<EnvelopeRenderProvider envelope={envelope} token={recipient.token}> <EnvelopeRenderProvider envelope={envelope} token={recipient.token}>
<EmbedSignDocumentV2ClientPage <EmbedSignDocumentV2ClientPage

View File

@ -8,7 +8,7 @@ import { SignFieldNumberDialog } from '~/components/dialogs/sign-field-number-di
type HandleNumberFieldClickOptions = { type HandleNumberFieldClickOptions = {
field: TFieldNumber; field: TFieldNumber;
number: string | null; number: number | null;
}; };
export const handleNumberFieldClick = async ( export const handleNumberFieldClick = async (

View File

@ -41,7 +41,6 @@
"@simplewebauthn/server": "^9.0.3", "@simplewebauthn/server": "^9.0.3",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"colord": "^2.9.3", "colord": "^2.9.3",
"content-disposition": "^0.5.4",
"framer-motion": "^10.12.8", "framer-motion": "^10.12.8",
"hono": "4.7.0", "hono": "4.7.0",
"hono-rate-limiter": "^0.4.2", "hono-rate-limiter": "^0.4.2",
@ -88,7 +87,6 @@
"@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-typescript": "^12.1.2", "@rollup/plugin-typescript": "^12.1.2",
"@simplewebauthn/types": "^9.0.1", "@simplewebauthn/types": "^9.0.1",
"@types/content-disposition": "^0.5.9",
"@types/formidable": "^2.0.6", "@types/formidable": "^2.0.6",
"@types/luxon": "^3.3.1", "@types/luxon": "^3.3.1",
"@types/node": "^20", "@types/node": "^20",
@ -106,5 +104,5 @@
"vite-plugin-babel-macros": "^1.0.6", "vite-plugin-babel-macros": "^1.0.6",
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"
}, },
"version": "2.0.6" "version": "1.13.1"
} }

View File

@ -1,192 +0,0 @@
import { sValidator } from '@hono/standard-validator';
import { EnvelopeType } from '@prisma/client';
import { Hono } from 'hono';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import type { HonoEnv } from '../../router';
import { handleEnvelopeItemFileRequest } from '../files/files.helpers';
import {
ZDownloadDocumentRequestParamsSchema,
ZDownloadEnvelopeItemRequestParamsSchema,
} from './download.types';
export const downloadRoute = new Hono<HonoEnv>()
/**
* Download an envelope item by its ID.
* Requires API key authentication via Authorization header.
*/
.get(
'/envelopeItem/:envelopeItemId/download',
sValidator('param', ZDownloadEnvelopeItemRequestParamsSchema),
async (c) => {
const logger = c.get('logger');
try {
const { envelopeItemId, version } = c.req.valid('param');
const authorizationHeader = c.req.header('authorization');
// Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx"
const [token] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0);
if (!token) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'API token was not provided',
});
}
const apiToken = await getApiTokenByToken({ token });
if (apiToken.user.disabled) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'User is disabled',
});
}
logger.info({
auth: 'api',
source: 'apiV2',
path: c.req.path,
userId: apiToken.user.id,
apiTokenId: apiToken.id,
envelopeItemId,
version,
});
const envelopeItem = await prisma.envelopeItem.findFirst({
where: {
id: envelopeItemId,
envelope: {
team: buildTeamWhereQuery({ teamId: apiToken.teamId, userId: apiToken.user.id }),
},
},
include: {
envelope: true,
documentData: true,
},
});
if (!envelopeItem) {
return c.json({ error: 'Envelope item not found' }, 404);
}
if (!envelopeItem.documentData) {
return c.json({ error: 'Document data not found' }, 404);
}
return await handleEnvelopeItemFileRequest({
title: envelopeItem.title,
status: envelopeItem.envelope.status,
documentData: envelopeItem.documentData,
version: version || 'signed',
isDownload: true,
context: c,
});
} catch (error) {
logger.error(error);
if (error instanceof AppError) {
if (error.code === AppErrorCode.UNAUTHORIZED) {
return c.json({ error: error.message }, 401);
}
return c.json({ error: error.message }, 400);
}
return c.json({ error: 'Internal server error' }, 500);
}
},
)
/**
* Download a document by its ID.
* Requires API key authentication via Authorization header.
*/
.get(
'/document/:documentId/download',
sValidator('param', ZDownloadDocumentRequestParamsSchema),
async (c) => {
const logger = c.get('logger');
try {
const { documentId, version } = c.req.valid('param');
const authorizationHeader = c.req.header('authorization');
// Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx"
const [token] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0);
if (!token) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'API token was not provided',
});
}
const apiToken = await getApiTokenByToken({ token });
if (apiToken.user.disabled) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'User is disabled',
});
}
logger.info({
auth: 'api',
source: 'apiV2',
path: c.req.path,
userId: apiToken.user.id,
apiTokenId: apiToken.id,
documentId,
version,
});
const envelope = await getEnvelopeById({
id: {
type: 'documentId',
id: documentId,
},
type: EnvelopeType.DOCUMENT,
userId: apiToken.user.id,
teamId: apiToken.teamId,
}).catch(() => null);
if (!envelope) {
return c.json({ error: 'Document not found' }, 404);
}
// Get the first envelope item (documents have exactly one)
const [envelopeItem] = envelope.envelopeItems;
if (!envelopeItem) {
return c.json({ error: 'Document item not found' }, 404);
}
if (!envelopeItem.documentData) {
return c.json({ error: 'Document data not found' }, 404);
}
return await handleEnvelopeItemFileRequest({
title: envelopeItem.title,
status: envelope.status,
documentData: envelopeItem.documentData,
version: version || 'signed',
isDownload: true,
context: c,
});
} catch (error) {
logger.error(error);
if (error instanceof AppError) {
if (error.code === AppErrorCode.UNAUTHORIZED) {
return c.json({ error: error.message }, 401);
}
return c.json({ error: error.message }, 400);
}
return c.json({ error: 'Internal server error' }, 500);
}
},
);

View File

@ -1,29 +0,0 @@
import { z } from 'zod';
export const ZDownloadEnvelopeItemRequestParamsSchema = z.object({
envelopeItemId: z.string().describe('The ID of the envelope item to download.'),
version: z
.enum(['original', 'signed'])
.optional()
.default('signed')
.describe(
'The version of the envelope item to download. "signed" returns the completed document with signatures, "original" returns the original uploaded document.',
),
});
export type TDownloadEnvelopeItemRequestParams = z.infer<
typeof ZDownloadEnvelopeItemRequestParamsSchema
>;
export const ZDownloadDocumentRequestParamsSchema = z.object({
documentId: z.coerce.number().describe('The ID of the document to download.'),
version: z
.enum(['original', 'signed'])
.optional()
.default('signed')
.describe(
'The version of the document to download. "signed" returns the completed document with signatures, "original" returns the original uploaded document.',
),
});
export type TDownloadDocumentRequestParams = z.infer<typeof ZDownloadDocumentRequestParamsSchema>;

View File

@ -1,11 +1,10 @@
import { type DocumentDataType, DocumentStatus } from '@prisma/client'; import { type DocumentDataType, DocumentStatus } from '@prisma/client';
import contentDisposition from 'content-disposition';
import { type Context } from 'hono'; import { type Context } from 'hono';
import { sha256 } from '@documenso/lib/universal/crypto'; import { sha256 } from '@documenso/lib/universal/crypto';
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server'; import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
import type { HonoEnv } from '../../router'; import type { HonoEnv } from '../router';
type HandleEnvelopeItemFileRequestOptions = { type HandleEnvelopeItemFileRequestOptions = {
title: string; title: string;
@ -35,7 +34,7 @@ export const handleEnvelopeItemFileRequest = async ({
const etag = Buffer.from(sha256(documentDataToUse)).toString('hex'); const etag = Buffer.from(sha256(documentDataToUse)).toString('hex');
if (c.req.header('If-None-Match') === etag && !isDownload) { if (c.req.header('If-None-Match') === etag) {
return c.body(null, 304); return c.body(null, 304);
} }
@ -59,7 +58,8 @@ export const handleEnvelopeItemFileRequest = async ({
if (status === DocumentStatus.COMPLETED) { if (status === DocumentStatus.COMPLETED) {
c.header('Cache-Control', 'public, max-age=31536000, immutable'); c.header('Cache-Control', 'public, max-age=31536000, immutable');
} else { } else {
c.header('Cache-Control', 'public, max-age=0, must-revalidate'); // Set a tiny 1 minute cache, with must-revalidate to ensure the client always checks for updates.
c.header('Cache-Control', 'public, max-age=60, must-revalidate');
} }
} }
@ -69,7 +69,7 @@ export const handleEnvelopeItemFileRequest = async ({
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf'; const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
const filename = `${baseTitle}${suffix}`; const filename = `${baseTitle}${suffix}`;
c.header('Content-Disposition', contentDisposition(filename)); c.header('Content-Disposition', `attachment; filename="${filename}"`);
// For downloads, prevent caching to ensure fresh data // For downloads, prevent caching to ensure fresh data
c.header('Cache-Control', 'no-cache, no-store, must-revalidate'); c.header('Cache-Control', 'no-cache, no-store, must-revalidate');

View File

@ -10,7 +10,7 @@ import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions'; import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import type { HonoEnv } from '../../router'; import type { HonoEnv } from '../router';
import { handleEnvelopeItemFileRequest } from './files.helpers'; import { handleEnvelopeItemFileRequest } from './files.helpers';
import { import {
type TGetPresignedPostUrlResponse, type TGetPresignedPostUrlResponse,

View File

@ -14,8 +14,7 @@ import { getIpAddress } from '@documenso/lib/universal/get-ip-address';
import { logger } from '@documenso/lib/utils/logger'; import { logger } from '@documenso/lib/utils/logger';
import { openApiDocument } from '@documenso/trpc/server/open-api'; import { openApiDocument } from '@documenso/trpc/server/open-api';
import { downloadRoute } from './api/download/download'; import { filesRoute } from './api/files';
import { filesRoute } from './api/files/files';
import { type AppContext, appContext } from './context'; import { type AppContext, appContext } from './context';
import { appMiddleware } from './middleware'; import { appMiddleware } from './middleware';
import { openApiTrpcServerHandler } from './trpc/hono-trpc-open-api'; import { openApiTrpcServerHandler } from './trpc/hono-trpc-open-api';
@ -93,8 +92,6 @@ app.use('/api/trpc/*', reactRouterTrpcServer);
// Unstable API server routes. Order matters for these two. // Unstable API server routes. Order matters for these two.
app.get(`${API_V2_URL}/openapi.json`, (c) => c.json(openApiDocument)); app.get(`${API_V2_URL}/openapi.json`, (c) => c.json(openApiDocument));
app.use(`${API_V2_URL}/*`, cors()); app.use(`${API_V2_URL}/*`, cors());
// Shadows the download routes that tRPC defines since tRPC-to-openapi doesn't support their return types.
app.route(`${API_V2_URL}`, downloadRoute);
app.use(`${API_V2_URL}/*`, async (c) => app.use(`${API_V2_URL}/*`, async (c) =>
openApiTrpcServerHandler(c, { openApiTrpcServerHandler(c, {
isBeta: false, isBeta: false,
@ -104,8 +101,6 @@ app.use(`${API_V2_URL}/*`, async (c) =>
// Unstable API server routes. Order matters for these two. // Unstable API server routes. Order matters for these two.
app.get(`${API_V2_BETA_URL}/openapi.json`, (c) => c.json(openApiDocument)); app.get(`${API_V2_BETA_URL}/openapi.json`, (c) => c.json(openApiDocument));
app.use(`${API_V2_BETA_URL}/*`, cors()); app.use(`${API_V2_BETA_URL}/*`, cors());
// Shadows the download routes that tRPC defines since tRPC-to-openapi doesn't support their return types.
app.route(`${API_V2_BETA_URL}`, downloadRoute);
app.use(`${API_V2_BETA_URL}/*`, async (c) => app.use(`${API_V2_BETA_URL}/*`, async (c) =>
openApiTrpcServerHandler(c, { openApiTrpcServerHandler(c, {
isBeta: true, isBeta: true,

Binary file not shown.

15
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "@documenso/root", "name": "@documenso/root",
"version": "2.0.6", "version": "1.13.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@documenso/root", "name": "@documenso/root",
"version": "2.0.6", "version": "1.13.1",
"workspaces": [ "workspaces": [
"apps/*", "apps/*",
"packages/*" "packages/*"
@ -100,7 +100,7 @@
}, },
"apps/remix": { "apps/remix": {
"name": "@documenso/remix", "name": "@documenso/remix",
"version": "2.0.6", "version": "1.13.1",
"dependencies": { "dependencies": {
"@cantoo/pdf-lib": "^2.5.2", "@cantoo/pdf-lib": "^2.5.2",
"@documenso/api": "*", "@documenso/api": "*",
@ -129,7 +129,6 @@
"@simplewebauthn/server": "^9.0.3", "@simplewebauthn/server": "^9.0.3",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"colord": "^2.9.3", "colord": "^2.9.3",
"content-disposition": "^0.5.4",
"framer-motion": "^10.12.8", "framer-motion": "^10.12.8",
"hono": "4.7.0", "hono": "4.7.0",
"hono-rate-limiter": "^0.4.2", "hono-rate-limiter": "^0.4.2",
@ -176,7 +175,6 @@
"@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-typescript": "^12.1.2", "@rollup/plugin-typescript": "^12.1.2",
"@simplewebauthn/types": "^9.0.1", "@simplewebauthn/types": "^9.0.1",
"@types/content-disposition": "^0.5.9",
"@types/formidable": "^2.0.6", "@types/formidable": "^2.0.6",
"@types/luxon": "^3.3.1", "@types/luxon": "^3.3.1",
"@types/node": "^20", "@types/node": "^20",
@ -12317,13 +12315,6 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/content-disposition": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.9.tgz",
"integrity": "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/cross-spawn": { "node_modules/@types/cross-spawn": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz",

View File

@ -1,6 +1,6 @@
{ {
"private": true, "private": true,
"version": "2.0.6", "version": "1.13.1",
"scripts": { "scripts": {
"build": "turbo run build", "build": "turbo run build",
"dev": "turbo run dev --filter=@documenso/remix", "dev": "turbo run dev --filter=@documenso/remix",
@ -95,4 +95,4 @@
"trigger.dev": { "trigger.dev": {
"endpointId": "documenso-app" "endpointId": "documenso-app"
} }
} }

View File

@ -1,6 +1,4 @@
import { FieldType } from '@prisma/client'; import { FieldType } from '@prisma/client';
import fs from 'node:fs';
import path from 'node:path';
import type { TFieldAndMeta } from '@documenso/lib/types/field-meta'; import type { TFieldAndMeta } from '@documenso/lib/types/field-meta';
import { toCheckboxCustomText } from '@documenso/lib/utils/fields'; import { toCheckboxCustomText } from '@documenso/lib/utils/fields';
@ -15,66 +13,11 @@ export type FieldTestData = TFieldAndMeta & {
signature?: string; signature?: string;
}; };
export const signatureBase64Demo = `data:image/png;base64,${fs.readFileSync(
path.join(__dirname, '../../../packages/assets/', 'logo_icon.png'),
'base64',
)}`;
const columnWidth = 19.125; const columnWidth = 19.125;
const fullColumnWidth = 57.37499999999998;
const rowHeight = 6.7; const rowHeight = 6.7;
const rowPadding = 0;
const calculatePositionPageOne = ( const alignmentGridStartX = 31;
row: number, const alignmentGridStartY = 19.02;
column: number,
width: 'full' | 'column' = 'column',
) => {
const alignmentGridStartX = 31;
const alignmentGridStartY = 19;
return {
height: rowHeight,
width: width === 'full' ? fullColumnWidth : columnWidth,
positionX: alignmentGridStartX + (column ?? 0) * columnWidth,
positionY: alignmentGridStartY + row * (rowHeight + rowPadding),
};
};
const calculatePositionPageTwo = (
row: number,
column: number,
width: 'full' | 'column' = 'column',
) => {
const alignmentGridStartX = 31;
const alignmentGridStartY = 16.35;
return {
height: rowHeight,
width: width === 'full' ? fullColumnWidth : columnWidth,
positionX: alignmentGridStartX + (column ?? 0) * columnWidth,
positionY: alignmentGridStartY + row * (rowHeight + rowPadding),
};
};
const calculatePositionPageThree = (
row: number,
column: number,
width: 'full' | 'column' = 'column',
rowQuantity: number = 1,
) => {
const alignmentGridStartX = 31;
const alignmentGridStartY = 16.4;
const rowHeight = 6.8;
return {
height: rowHeight * rowQuantity,
width: width === 'full' ? fullColumnWidth : columnWidth,
positionX: alignmentGridStartX + (column ?? 0) * columnWidth,
positionY: alignmentGridStartY + row * (rowHeight + rowPadding),
};
};
export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
/** /**
@ -88,7 +31,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'email', type: 'email',
}, },
page: 1, page: 1,
...calculatePositionPageOne(0, 0), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'admin@documenso.com', customText: 'admin@documenso.com',
}, },
{ {
@ -98,7 +44,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'email', type: 'email',
}, },
page: 1, page: 1,
...calculatePositionPageOne(0, 1), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'admin@documenso.com', customText: 'admin@documenso.com',
}, },
{ {
@ -109,7 +58,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'email', type: 'email',
}, },
page: 1, page: 1,
...calculatePositionPageOne(0, 2), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'admin@documenso.com', customText: 'admin@documenso.com',
}, },
/** /**
@ -123,7 +75,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'name', type: 'name',
}, },
page: 1, page: 1,
...calculatePositionPageOne(1, 0), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'John Doe', customText: 'John Doe',
}, },
{ {
@ -133,7 +88,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'name', type: 'name',
}, },
page: 1, page: 1,
...calculatePositionPageOne(1, 1), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'John Doe', customText: 'John Doe',
}, },
{ {
@ -144,7 +102,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'name', type: 'name',
}, },
page: 1, page: 1,
...calculatePositionPageOne(1, 2), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'John Doe', customText: 'John Doe',
}, },
/** /**
@ -158,7 +119,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'date', type: 'date',
}, },
page: 1, page: 1,
...calculatePositionPageOne(2, 0), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '123456789', customText: '123456789',
}, },
{ {
@ -168,7 +132,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'date', type: 'date',
}, },
page: 1, page: 1,
...calculatePositionPageOne(2, 1), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '123456789', customText: '123456789',
}, },
{ {
@ -179,7 +146,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'date', type: 'date',
}, },
page: 1, page: 1,
...calculatePositionPageOne(2, 2), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '123456789', customText: '123456789',
}, },
/** /**
@ -193,7 +163,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'text', type: 'text',
}, },
page: 1, page: 1,
...calculatePositionPageOne(3, 0), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '123456789', customText: '123456789',
}, },
{ {
@ -203,7 +176,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'text', type: 'text',
}, },
page: 1, page: 1,
...calculatePositionPageOne(3, 1), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '123456789', customText: '123456789',
}, },
{ {
@ -214,7 +190,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'text', type: 'text',
}, },
page: 1, page: 1,
...calculatePositionPageOne(3, 2), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '123456789', customText: '123456789',
}, },
/** /**
@ -228,7 +207,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'number', type: 'number',
}, },
page: 1, page: 1,
...calculatePositionPageOne(4, 0), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '123456789', customText: '123456789',
}, },
{ {
@ -238,7 +220,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'number', type: 'number',
}, },
page: 1, page: 1,
...calculatePositionPageOne(4, 1), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '123456789', customText: '123456789',
}, },
{ {
@ -249,7 +234,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'number', type: 'number',
}, },
page: 1, page: 1,
...calculatePositionPageOne(4, 2), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '123456789', customText: '123456789',
}, },
/** /**
@ -263,7 +251,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'initials', type: 'initials',
}, },
page: 1, page: 1,
...calculatePositionPageOne(5, 0), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'JD', customText: 'JD',
}, },
{ {
@ -273,7 +264,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'initials', type: 'initials',
}, },
page: 1, page: 1,
...calculatePositionPageOne(5, 1), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'JD', customText: 'JD',
}, },
{ {
@ -284,7 +278,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'initials', type: 'initials',
}, },
page: 1, page: 1,
...calculatePositionPageOne(5, 2), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'JD', customText: 'JD',
}, },
/** /**
@ -302,7 +299,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
], ],
}, },
page: 1, page: 1,
...calculatePositionPageOne(6, 0), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '0', customText: '0',
}, },
{ {
@ -312,12 +312,15 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'radio', type: 'radio',
values: [ values: [
{ id: 1, checked: false, value: 'Option 1' }, { id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: false, value: 'Option 2' }, { id: 2, checked: true, value: 'Option 2' },
], ],
}, },
page: 1, page: 1,
...calculatePositionPageOne(6, 1), height: rowHeight,
customText: '', width: columnWidth,
positionX: 0,
positionY: 0,
customText: '2',
}, },
{ {
type: FieldType.RADIO, type: FieldType.RADIO,
@ -327,12 +330,15 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'radio', type: 'radio',
values: [ values: [
{ id: 1, checked: false, value: 'Option 1' }, { id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: true, value: 'Option 2' }, { id: 2, checked: false, value: 'Option 2' },
], ],
}, },
page: 1, page: 1,
...calculatePositionPageOne(6, 2), height: rowHeight,
customText: '1', width: columnWidth,
positionX: 0,
positionY: 0,
customText: '',
}, },
/** /**
* Row 8 Checkbox * Row 8 Checkbox
@ -349,7 +355,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
], ],
}, },
page: 1, page: 1,
...calculatePositionPageOne(7, 0), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: toCheckboxCustomText([0]), customText: toCheckboxCustomText([0]),
}, },
{ {
@ -359,12 +368,15 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'checkbox', type: 'checkbox',
values: [ values: [
{ id: 1, checked: false, value: 'Option 1' }, { id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: false, value: 'Option 2' }, { id: 2, checked: true, value: 'Option 2' },
], ],
}, },
page: 1, page: 1,
...calculatePositionPageOne(7, 1), height: rowHeight,
customText: '', width: columnWidth,
positionX: 0,
positionY: 0,
customText: toCheckboxCustomText([1]),
}, },
{ {
type: FieldType.CHECKBOX, type: FieldType.CHECKBOX,
@ -374,12 +386,15 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'checkbox', type: 'checkbox',
values: [ values: [
{ id: 1, checked: false, value: 'Option 1' }, { id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: true, value: 'Option 2' }, { id: 2, checked: false, value: 'Option 2' },
], ],
}, },
page: 1, page: 1,
...calculatePositionPageOne(7, 2), height: rowHeight,
customText: toCheckboxCustomText([1]), width: columnWidth,
positionX: 0,
positionY: 0,
customText: '',
}, },
/** /**
* Row 8 Dropdown * Row 8 Dropdown
@ -392,7 +407,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'dropdown', type: 'dropdown',
}, },
page: 1, page: 1,
...calculatePositionPageOne(8, 0), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'Option 1', customText: 'Option 1',
}, },
{ {
@ -402,7 +420,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'dropdown', type: 'dropdown',
}, },
page: 1, page: 1,
...calculatePositionPageOne(8, 1), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'Option 1', customText: 'Option 1',
}, },
{ {
@ -413,7 +434,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'dropdown', type: 'dropdown',
}, },
page: 1, page: 1,
...calculatePositionPageOne(8, 2), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'Option 1', customText: 'Option 1',
}, },
/** /**
@ -426,7 +450,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'signature', type: 'signature',
}, },
page: 1, page: 1,
...calculatePositionPageOne(9, 0), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '', customText: '',
signature: 'My Signature', signature: 'My Signature',
}, },
@ -436,7 +463,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'signature', type: 'signature',
}, },
page: 1, page: 1,
...calculatePositionPageOne(9, 1), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '', customText: '',
signature: 'My Signature', signature: 'My Signature',
}, },
@ -447,295 +477,22 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
type: 'signature', type: 'signature',
}, },
page: 1, page: 1,
...calculatePositionPageOne(9, 2), height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '', customText: '',
signature: 'My Signature', signature: 'My Signature',
}, },
/**
* @@@@@@@@@@@@@@@@@@@@@@@
*
* PAGE 2
*
* @@@@@@@@@@@@@@@@@@@@@@@
*/
// TEXT GRID ROW 1
{
type: FieldType.TEXT,
fieldMeta: {
textAlign: 'left',
type: 'text',
verticalAlign: 'top',
},
page: 2,
...calculatePositionPageTwo(0, 0),
customText: 'SOME TEXT',
},
{
type: FieldType.TEXT,
fieldMeta: {
textAlign: 'center',
type: 'text',
verticalAlign: 'top',
},
page: 2,
...calculatePositionPageTwo(0, 1),
customText: 'SOME TEXT',
},
{
type: FieldType.TEXT,
fieldMeta: {
textAlign: 'right',
type: 'text',
verticalAlign: 'top',
},
page: 2,
...calculatePositionPageTwo(0, 2),
customText: 'SOME TEXT',
},
// TEXT GRID ROW 2
{
type: FieldType.TEXT,
fieldMeta: {
textAlign: 'left',
type: 'text',
verticalAlign: 'middle',
},
page: 2,
...calculatePositionPageTwo(1, 0),
customText: 'SOME TEXT',
},
{
type: FieldType.TEXT,
fieldMeta: {
textAlign: 'center',
type: 'text',
verticalAlign: 'middle',
},
page: 2,
...calculatePositionPageTwo(1, 1),
customText: 'SOME TEXT',
},
{
type: FieldType.TEXT,
fieldMeta: {
textAlign: 'right',
type: 'text',
verticalAlign: 'middle',
},
page: 2,
...calculatePositionPageTwo(1, 2),
customText: 'SOME TEXT',
},
// TEXT GRID ROW 3
{
type: FieldType.TEXT,
fieldMeta: {
textAlign: 'left',
type: 'text',
verticalAlign: 'bottom',
},
page: 2,
...calculatePositionPageTwo(2, 0),
customText: 'SOME TEXT',
},
{
type: FieldType.TEXT,
fieldMeta: {
textAlign: 'center',
type: 'text',
verticalAlign: 'bottom',
},
page: 2,
...calculatePositionPageTwo(2, 1),
customText: 'SOME TEXT',
},
{
type: FieldType.TEXT,
fieldMeta: {
textAlign: 'right',
type: 'text',
verticalAlign: 'bottom',
},
page: 2,
...calculatePositionPageTwo(2, 2),
customText: 'SOME TEXT',
},
// NUMBER GRID ROW 1
{
type: FieldType.NUMBER,
fieldMeta: {
textAlign: 'left',
type: 'number',
verticalAlign: 'top',
},
page: 2,
...calculatePositionPageTwo(3, 0),
customText: '123456789123456789',
},
{
type: FieldType.NUMBER,
fieldMeta: {
textAlign: 'center',
type: 'number',
verticalAlign: 'top',
},
page: 2,
...calculatePositionPageTwo(3, 1),
customText: '123456789123456789',
},
{
type: FieldType.NUMBER,
fieldMeta: {
textAlign: 'right',
type: 'number',
verticalAlign: 'top',
},
page: 2,
...calculatePositionPageTwo(3, 2),
customText: '123456789123456789',
},
// NUMBER GRID ROW 2
{
type: FieldType.NUMBER,
fieldMeta: {
textAlign: 'left',
type: 'number',
verticalAlign: 'middle',
},
page: 2,
...calculatePositionPageTwo(4, 0),
customText: '123456789123456789',
},
{
type: FieldType.NUMBER,
fieldMeta: {
textAlign: 'center',
type: 'number',
verticalAlign: 'middle',
},
page: 2,
...calculatePositionPageTwo(4, 1),
customText: '123456789123456789',
},
{
type: FieldType.NUMBER,
fieldMeta: {
textAlign: 'right',
type: 'number',
verticalAlign: 'middle',
},
page: 2,
...calculatePositionPageTwo(4, 2),
customText: '123456789123456789',
},
// NUMBER GRID ROW 3
{
type: FieldType.NUMBER,
fieldMeta: {
textAlign: 'left',
type: 'number',
verticalAlign: 'bottom',
},
page: 2,
...calculatePositionPageTwo(5, 0),
customText: '123456789123456789',
},
{
type: FieldType.NUMBER,
fieldMeta: {
textAlign: 'center',
type: 'number',
verticalAlign: 'bottom',
},
page: 2,
...calculatePositionPageTwo(5, 1),
customText: '123456789123456789',
},
{
type: FieldType.NUMBER,
fieldMeta: {
textAlign: 'right',
type: 'number',
verticalAlign: 'bottom',
},
page: 2,
...calculatePositionPageTwo(5, 2),
customText: '123456789123456789',
},
// Text combing
{
type: FieldType.TEXT,
fieldMeta: {
type: 'text',
verticalAlign: 'middle',
letterSpacing: 32,
characterLimit: 9,
},
page: 2,
...calculatePositionPageTwo(6, 0, 'full'),
positionX: calculatePositionPageTwo(6, 0, 'full').positionX + 1.75,
width: calculatePositionPageTwo(6, 0, 'full').width + 1.75,
customText: 'HEY HEY 1',
},
// Number combing
{
type: FieldType.NUMBER,
fieldMeta: {
type: 'number',
verticalAlign: 'middle',
letterSpacing: 32,
},
page: 2,
...calculatePositionPageTwo(7, 0, 'full'),
positionX: calculatePositionPageTwo(7, 0, 'full').positionX + 1.75,
width: calculatePositionPageTwo(7, 0, 'full').width + 1.75,
customText: '123456789',
},
/**
* @@@@@@@@@@@@@@@@@@@@@@@
*
* PAGE 2 TEXT MULTILINE
*
* @@@@@@@@@@@@@@@@@@@@@@@
*/
{
type: FieldType.TEXT,
fieldMeta: {
verticalAlign: 'top',
textAlign: 'left',
lineHeight: 2.24,
type: 'text',
},
page: 3,
...calculatePositionPageThree(0, 0, 'full', 3),
customText:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
},
{
type: FieldType.TEXT,
fieldMeta: {
verticalAlign: 'middle',
textAlign: 'center',
lineHeight: 2.24,
type: 'text',
},
page: 3,
...calculatePositionPageThree(3, 0, 'full', 3),
customText:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
},
{
type: FieldType.TEXT,
fieldMeta: {
verticalAlign: 'bottom',
textAlign: 'right',
lineHeight: 2.24,
type: 'text',
},
page: 3,
...calculatePositionPageThree(6, 0, 'full', 3),
customText:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
},
] as const; ] as const;
export const formatAlignmentTestFields = ALIGNMENT_TEST_FIELDS.map((field, index) => {
const row = Math.floor(index / 3);
const column = index % 3;
return {
...field,
positionX: alignmentGridStartX + column * columnWidth,
positionY: alignmentGridStartY + row * rowHeight,
};
});

View File

@ -7,7 +7,6 @@ import {
} from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants'; } from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
import type { FieldTestData } from './field-alignment-pdf'; import type { FieldTestData } from './field-alignment-pdf';
import { signatureBase64Demo } from './field-alignment-pdf';
const columnWidth = 20.1; const columnWidth = 20.1;
const fullColumnWidth = 75.8; const fullColumnWidth = 75.8;
@ -38,7 +37,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
page: 2, page: 2,
...calculatePosition(0, 0), ...calculatePosition(0, 0),
customText: '', customText: '',
signature: signatureBase64Demo, signature: 'My Signature',
}, },
{ {
type: FieldType.SIGNATURE, type: FieldType.SIGNATURE,
@ -48,7 +47,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
page: 2, page: 2,
...calculatePosition(1, 0), ...calculatePosition(1, 0),
customText: '', customText: '',
signature: signatureBase64Demo, signature: 'My Signature',
}, },
{ {
type: FieldType.SIGNATURE, type: FieldType.SIGNATURE,
@ -68,7 +67,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
page: 2, page: 2,
...calculatePosition(3, 0), ...calculatePosition(3, 0),
customText: '', customText: '',
signature: 'My Signature super overflow maybe', signature: 'My Signature',
}, },
/** /**
@ -81,7 +80,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
}, },
page: 3, page: 3,
...calculatePosition(0, 0, 'full'), ...calculatePosition(0, 0, 'full'),
customText: 'Hello world, this is some random text that I have written here', customText: '123456789',
}, },
{ {
type: FieldType.TEXT, type: FieldType.TEXT,
@ -90,7 +89,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
}, },
page: 3, page: 3,
...calculatePosition(1, 0), ...calculatePosition(1, 0),
customText: 'Some text that should overflow correctly', customText: '123456789123456789123456789123456789',
}, },
{ {
type: FieldType.TEXT, type: FieldType.TEXT,
@ -110,7 +109,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
}, },
page: 3, page: 3,
...calculatePosition(3, 0), ...calculatePosition(3, 0),
customText: 'Input should have a placeholder text when clicked', customText: '123456789',
}, },
{ {
type: FieldType.TEXT, type: FieldType.TEXT,
@ -120,7 +119,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
}, },
page: 3, page: 3,
...calculatePosition(3, 1), ...calculatePosition(3, 1),
customText: 'Should have a label during editing and signing', customText: '123456789',
}, },
{ {
type: FieldType.TEXT, type: FieldType.TEXT,
@ -130,7 +129,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
}, },
page: 3, page: 3,
...calculatePosition(3, 2), ...calculatePosition(3, 2),
customText: '', customText: '123456789',
}, },
{ {
type: FieldType.TEXT, type: FieldType.TEXT,
@ -140,19 +139,20 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
}, },
page: 3, page: 3,
...calculatePosition(4, 0), ...calculatePosition(4, 0),
customText: 'This is a required field', customText: '123456789',
}, },
{ {
type: FieldType.TEXT, type: FieldType.TEXT,
fieldMeta: { fieldMeta: {
type: 'text', type: 'text',
readOnly: true, readOnly: true,
text: 'Some Readonly Value', text: 'Readonly Value',
}, },
page: 3, page: 3,
...calculatePosition(4, 1), ...calculatePosition(4, 1),
customText: '', customText: 'Readonly Value',
}, },
/** /**
* PAGE 4 NUMBER * PAGE 4 NUMBER
*/ */
@ -220,7 +220,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
type: FieldType.NUMBER, type: FieldType.NUMBER,
fieldMeta: { fieldMeta: {
type: 'number', type: 'number',
value: '123456789', value: '123',
}, },
page: 4, page: 4,
...calculatePosition(3, 2), ...calculatePosition(3, 2),
@ -241,11 +241,10 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
fieldMeta: { fieldMeta: {
type: 'number', type: 'number',
readOnly: true, readOnly: true,
value: '123456789',
}, },
page: 4, page: 4,
...calculatePosition(4, 1), ...calculatePosition(4, 1),
customText: '', customText: '123456789',
}, },
/** /**
@ -273,8 +272,8 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
type: 'radio', type: 'radio',
values: [ values: [
{ id: 1, checked: false, value: 'Option 1' }, { id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: false, value: 'Option 2' }, { id: 2, checked: true, value: 'Option 2' },
{ id: 3, checked: true, value: 'Option 3' }, { id: 3, checked: false, value: 'Option 3' },
], ],
}, },
page: 5, page: 5,
@ -286,7 +285,6 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
fieldMeta: { fieldMeta: {
direction: 'vertical', direction: 'vertical',
type: 'radio', type: 'radio',
required: true,
values: [ values: [
{ id: 1, checked: false, value: 'Option 1' }, { id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: false, value: 'Option 2' }, { id: 2, checked: false, value: 'Option 2' },
@ -295,18 +293,17 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
}, },
page: 5, page: 5,
...calculatePosition(2, 0), ...calculatePosition(2, 0),
customText: '2', customText: '',
}, },
{ {
type: FieldType.RADIO, type: FieldType.RADIO,
fieldMeta: { fieldMeta: {
direction: 'vertical', direction: 'vertical',
type: 'radio', type: 'radio',
readOnly: true,
values: [ values: [
{ id: 1, checked: false, value: 'Option 1' }, { id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: false, value: 'Option 2' }, { id: 2, checked: false, value: 'Option 2' },
{ id: 3, checked: true, value: 'Option 3' }, { id: 3, checked: false, value: 'Option 3' },
], ],
}, },
page: 5, page: 5,
@ -341,7 +338,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
values: [ values: [
{ id: 1, checked: false, value: 'Option 1' }, { id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: true, value: 'Option 2' }, { id: 2, checked: true, value: 'Option 2' },
{ id: 3, checked: false, value: 'Option 3' }, { id: 2, checked: true, value: 'Option 3' },
], ],
}, },
page: 6, page: 6,
@ -361,7 +358,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
}, },
page: 6, page: 6,
...calculatePosition(2, 0), ...calculatePosition(2, 0),
customText: toCheckboxCustomText([2]), customText: '',
}, },
{ {
type: FieldType.CHECKBOX, type: FieldType.CHECKBOX,
@ -371,7 +368,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
readOnly: true, readOnly: true,
values: [ values: [
{ id: 1, checked: false, value: 'Option 1' }, { id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: true, value: 'Option 2' }, { id: 2, checked: false, value: 'Option 2' },
], ],
}, },
page: 6, page: 6,
@ -448,11 +445,11 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
fieldMeta: { fieldMeta: {
values: [{ value: 'Option 1' }, { value: 'Option 2' }], values: [{ value: 'Option 1' }, { value: 'Option 2' }],
type: 'dropdown', type: 'dropdown',
defaultValue: 'Option 2', defaultValue: 'Option 1',
}, },
page: 7, page: 7,
...calculatePosition(1, 0), ...calculatePosition(1, 0),
customText: 'Option 2', customText: 'Option 1',
}, },
{ {
type: FieldType.DROPDOWN, type: FieldType.DROPDOWN,
@ -463,14 +460,13 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
}, },
page: 7, page: 7,
...calculatePosition(2, 0), ...calculatePosition(2, 0),
customText: 'Option 3', customText: 'Option 1',
}, },
{ {
type: FieldType.DROPDOWN, type: FieldType.DROPDOWN,
fieldMeta: { fieldMeta: {
values: [{ value: 'Option 1' }, { value: 'Option 2' }, { value: 'Option 3' }], values: [{ value: 'Option 1' }, { value: 'Option 2' }, { value: 'Option 3' }],
type: 'dropdown', type: 'dropdown',
defaultValue: 'Option 1',
readOnly: true, readOnly: true,
}, },
page: 7, page: 7,

View File

@ -27,7 +27,7 @@ import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/en
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types'; import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types'; import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types';
import { ALIGNMENT_TEST_FIELDS } from '../../../constants/field-alignment-pdf'; import { formatAlignmentTestFields } from '../../../constants/field-alignment-pdf';
import { FIELD_META_TEST_FIELDS } from '../../../constants/field-meta-pdf'; import { FIELD_META_TEST_FIELDS } from '../../../constants/field-meta-pdf';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL(); const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
@ -490,7 +490,7 @@ test.describe('API V2 Envelopes', () => {
// Step 6: Create fields for first PDF (alignment fields) // Step 6: Create fields for first PDF (alignment fields)
const alignmentFieldsRequest = { const alignmentFieldsRequest = {
envelopeId: createdEnvelope.id, envelopeId: createdEnvelope.id,
data: ALIGNMENT_TEST_FIELDS.map((field) => ({ data: formatAlignmentTestFields.map((field) => ({
recipientId, recipientId,
envelopeItemId: alignmentItem.id, envelopeItemId: alignmentItem.id,
type: field.type, type: field.type,
@ -547,7 +547,7 @@ test.describe('API V2 Envelopes', () => {
expect(finalEnvelope.envelopeItems.length).toBe(2); expect(finalEnvelope.envelopeItems.length).toBe(2);
expect(finalEnvelope.recipients.length).toBe(1); expect(finalEnvelope.recipients.length).toBe(1);
expect(finalEnvelope.fields.length).toBe( expect(finalEnvelope.fields.length).toBe(
ALIGNMENT_TEST_FIELDS.length + FIELD_META_TEST_FIELDS.length, formatAlignmentTestFields.length + FIELD_META_TEST_FIELDS.length,
); );
expect(finalEnvelope.title).toBe('Envelope Full Field Test'); expect(finalEnvelope.title).toBe('Envelope Full Field Test');
expect(finalEnvelope.type).toBe(EnvelopeType.DOCUMENT); expect(finalEnvelope.type).toBe(EnvelopeType.DOCUMENT);

View File

@ -21,226 +21,34 @@ import pixelMatch from 'pixelmatch';
import { PNG } from 'pngjs'; import { PNG } from 'pngjs';
import type { TestInfo } from '@playwright/test'; import type { TestInfo } from '@playwright/test';
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { DocumentStatus, EnvelopeType } from '@prisma/client'; import { DocumentStatus } from '@prisma/client';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js'; import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js';
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download'; import { getEnvelopeDownloadUrl } from '@documenso/lib/utils/envelope-download';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { seedAlignmentTestDocument } from '@documenso/prisma/seed/initial-seed'; import { seedAlignmentTestDocument } from '@documenso/prisma/seed/initial-seed';
import { seedUser } from '@documenso/prisma/seed/users'; import { seedUser } from '@documenso/prisma/seed/users';
import { apiSignin } from '../fixtures/authentication';
import type {
TCreateEnvelopePayload,
TCreateEnvelopeResponse,
} from '../../../trpc/server/envelope-router/create-envelope.types';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../lib/constants/app';
import { createApiToken } from '../../../lib/server-only/public-api/create-api-token';
import { RecipientRole } from '../../../prisma/generated/types';
import { FIELD_META_TEST_FIELDS } from '../../constants/field-meta-pdf';
import { ALIGNMENT_TEST_FIELDS } from '../../constants/field-alignment-pdf';
import type { TDistributeEnvelopeRequest } from '../../../trpc/server/envelope-router/distribute-envelope.types';
import { isBase64Image } from '../../../lib/constants/signatures';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL(); import { apiSignin } from '../fixtures/authentication';
const baseUrl = `${WEBAPP_BASE_URL}/api/v2`;
test.describe.configure({ mode: 'parallel', timeout: 60000 }); test.describe.configure({ mode: 'parallel', timeout: 60000 });
test.skip('seed alignment test document', async ({ page }) => { test.skip('field placement visual regression', async ({ page }, testInfo) => {
const user = await prisma.user.findFirstOrThrow({
where: {
email: 'example@documenso.com',
},
include: {
ownedOrganisations: {
include: {
teams: true,
},
},
},
});
const userId = user.id;
const teamId = user.ownedOrganisations[0].teams[0].id;
await seedAlignmentTestDocument({
userId,
teamId,
recipientName: user.name || '',
recipientEmail: user.email,
insertFields: false,
status: DocumentStatus.DRAFT,
});
});
test('field placement visual regression', async ({ page, request }, testInfo) => {
const { user, team } = await seedUser(); const { user, team } = await seedUser();
const { token } = await createApiToken({ const envelope = await seedAlignmentTestDocument({
userId: user.id, userId: user.id,
teamId: team.id, teamId: team.id,
tokenName: 'test', recipientName: user.name || '',
expiresIn: null, recipientEmail: user.email,
insertFields: true,
status: DocumentStatus.PENDING,
}); });
// Step 1: Create initial envelope with Prisma (with first envelope item) const token = envelope.recipients[0].token;
const alignmentPdf = fs.readFileSync(
path.join(__dirname, '../../../../assets/field-font-alignment.pdf'),
);
const fieldMetaPdf = fs.readFileSync(path.join(__dirname, '../../../../assets/field-meta.pdf')); const signUrl = `/sign/${token}`;
const formData = new FormData();
const fieldMetaFields = FIELD_META_TEST_FIELDS.map((field) => ({
identifier: 'field-meta',
type: field.type,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
fieldMeta: field.fieldMeta,
}));
const alignmentFields = ALIGNMENT_TEST_FIELDS.map((field) => ({
identifier: 'alignment-pdf',
type: field.type,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
fieldMeta: field.fieldMeta,
}));
const createEnvelopePayload: TCreateEnvelopePayload = {
type: EnvelopeType.DOCUMENT,
title: 'Envelope Full Field Test',
recipients: [
{
email: user.email,
name: user.name || '',
role: RecipientRole.SIGNER,
fields: [...fieldMetaFields, ...alignmentFields],
},
],
};
formData.append('payload', JSON.stringify(createEnvelopePayload));
formData.append('files', new File([alignmentPdf], 'alignment-pdf', { type: 'application/pdf' }));
formData.append('files', new File([fieldMetaPdf], 'field-meta', { type: 'application/pdf' }));
const createEnvelopeRequest = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${token}` },
multipart: formData,
});
expect(createEnvelopeRequest.ok()).toBeTruthy();
expect(createEnvelopeRequest.status()).toBe(200);
const { id: createdEnvelopeId }: TCreateEnvelopeResponse = await createEnvelopeRequest.json();
const envelope = await prisma.envelope.findUniqueOrThrow({
where: {
id: createdEnvelopeId,
},
include: {
recipients: true,
envelopeItems: true,
},
});
const recipientId = envelope.recipients[0].id;
const alignmentItem = envelope.envelopeItems.find((item: { order: number }) => item.order === 1);
const fieldMetaItem = envelope.envelopeItems.find((item: { order: number }) => item.order === 2);
expect(recipientId).toBeDefined();
expect(alignmentItem).toBeDefined();
expect(fieldMetaItem).toBeDefined();
if (!alignmentItem || !fieldMetaItem) {
throw new Error('Envelope items not found');
}
const distributeEnvelopeRequest = await request.post(`${baseUrl}/envelope/distribute`, {
headers: { Authorization: `Bearer ${token}` },
data: {
envelopeId: envelope.id,
} satisfies TDistributeEnvelopeRequest,
});
expect(distributeEnvelopeRequest.ok()).toBeTruthy();
const uninsertedFields = await prisma.field.findMany({
where: {
envelopeId: envelope.id,
inserted: false,
},
include: {
envelopeItem: {
select: {
title: true,
},
},
},
});
await Promise.all(
uninsertedFields.map(async (field) => {
let foundField = ALIGNMENT_TEST_FIELDS.find(
(f) =>
field.page === f.page &&
field.envelopeItem.title === 'alignment-pdf' &&
Number(field.positionX).toFixed(2) === f.positionX.toFixed(2) &&
Number(field.positionY).toFixed(2) === f.positionY.toFixed(2) &&
Number(field.width).toFixed(2) === f.width.toFixed(2) &&
Number(field.height).toFixed(2) === f.height.toFixed(2),
);
if (!foundField) {
foundField = FIELD_META_TEST_FIELDS.find(
(f) =>
field.page === f.page &&
field.envelopeItem.title === 'field-meta' &&
Number(field.positionX).toFixed(2) === f.positionX.toFixed(2) &&
Number(field.positionY).toFixed(2) === f.positionY.toFixed(2) &&
Number(field.width).toFixed(2) === f.width.toFixed(2) &&
Number(field.height).toFixed(2) === f.height.toFixed(2),
);
}
if (!foundField) {
throw new Error('Field not found');
}
await prisma.field.update({
where: {
id: field.id,
},
data: {
inserted: true,
customText: foundField.customText,
signature: foundField.signature
? {
create: {
recipientId: envelope.recipients[0].id,
signatureImageAsBase64: isBase64Image(foundField.signature)
? foundField.signature
: null,
typedSignature: isBase64Image(foundField.signature) ? null : foundField.signature,
},
}
: undefined,
},
});
}),
);
const recipientToken = envelope.recipients[0].token;
const signUrl = `/sign/${recipientToken}`;
await apiSignin({ await apiSignin({
page, page,
@ -286,10 +94,9 @@ test('field placement visual regression', async ({ page, request }, testInfo) =>
await Promise.all( await Promise.all(
completedDocument.envelopeItems.map(async (item) => { completedDocument.envelopeItems.map(async (item) => {
const documentUrl = getEnvelopeItemPdfUrl({ const documentUrl = getEnvelopeDownloadUrl({
type: 'download',
envelopeItem: item, envelopeItem: item,
token: recipientToken, token,
version: 'signed', version: 'signed',
}); });
@ -372,8 +179,7 @@ test.skip('download envelope images', async ({ page }) => {
await Promise.all( await Promise.all(
completedDocument.envelopeItems.map(async (item) => { completedDocument.envelopeItems.map(async (item) => {
const documentUrl = getEnvelopeItemPdfUrl({ const documentUrl = getEnvelopeDownloadUrl({
type: 'download',
envelopeItem: item, envelopeItem: item,
token, token,
version: 'signed', version: 'signed',
@ -481,7 +287,7 @@ const compareSignedPdfWithImages = async ({
// Expect the certificate to NOT be blank. Since the storedImage is blank. // Expect the certificate to NOT be blank. Since the storedImage is blank.
expect.soft(comparison).toBeGreaterThan(20000); expect.soft(comparison).toBeGreaterThan(20000);
} else { } else {
expect.soft(comparison).toBeLessThan(2); expect.soft(comparison).toEqual(0);
} }
} }
}; };

View File

@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test';
import { DocumentStatus, FieldType } from '@prisma/client'; import { DocumentStatus, FieldType } from '@prisma/client';
import { getDocumentByToken } from '@documenso/lib/server-only/document/get-document-by-token'; import { getDocumentByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download'; import { getEnvelopeDownloadUrl } from '@documenso/lib/utils/envelope-download';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents'; import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
import { seedTeam } from '@documenso/prisma/seed/teams'; import { seedTeam } from '@documenso/prisma/seed/teams';
@ -34,8 +34,7 @@ test.describe('Signing Certificate Tests', () => {
}, },
}) })
.then(async (data) => { .then(async (data) => {
const documentUrl = getEnvelopeItemPdfUrl({ const documentUrl = getEnvelopeDownloadUrl({
type: 'download',
envelopeItem: data, envelopeItem: data,
token: recipient.token, token: recipient.token,
version: 'signed', version: 'signed',
@ -86,8 +85,7 @@ test.describe('Signing Certificate Tests', () => {
const firstDocumentData = completedDocument.envelopeItems[0]; const firstDocumentData = completedDocument.envelopeItems[0];
const documentUrl = getEnvelopeItemPdfUrl({ const documentUrl = getEnvelopeDownloadUrl({
type: 'download',
envelopeItem: firstDocumentData, envelopeItem: firstDocumentData,
token: recipient.token, token: recipient.token,
version: 'signed', version: 'signed',
@ -141,8 +139,7 @@ test.describe('Signing Certificate Tests', () => {
}, },
}) })
.then(async (data) => { .then(async (data) => {
const documentUrl = getEnvelopeItemPdfUrl({ const documentUrl = getEnvelopeDownloadUrl({
type: 'download',
envelopeItem: data, envelopeItem: data,
token: recipient.token, token: recipient.token,
version: 'signed', version: 'signed',
@ -191,8 +188,7 @@ test.describe('Signing Certificate Tests', () => {
const firstDocumentData = completedDocument.envelopeItems[0]; const firstDocumentData = completedDocument.envelopeItems[0];
const documentUrl = getEnvelopeItemPdfUrl({ const documentUrl = getEnvelopeDownloadUrl({
type: 'download',
envelopeItem: firstDocumentData, envelopeItem: firstDocumentData,
token: recipient.token, token: recipient.token,
version: 'signed', version: 'signed',
@ -246,8 +242,7 @@ test.describe('Signing Certificate Tests', () => {
}, },
}) })
.then(async (data) => { .then(async (data) => {
const documentUrl = getEnvelopeItemPdfUrl({ const documentUrl = getEnvelopeDownloadUrl({
type: 'download',
envelopeItem: data, envelopeItem: data,
token: recipient.token, token: recipient.token,
version: 'signed', version: 'signed',
@ -294,8 +289,7 @@ test.describe('Signing Certificate Tests', () => {
}, },
}); });
const documentUrl = getEnvelopeItemPdfUrl({ const documentUrl = getEnvelopeDownloadUrl({
type: 'download',
envelopeItem: completedDocument.envelopeItems[0], envelopeItem: completedDocument.envelopeItems[0],
token: recipient.token, token: recipient.token,
version: 'signed', version: 'signed',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

View File

@ -5,7 +5,6 @@ import { deleteCookie } from 'hono/cookie';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user'; import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user';
import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import type { OAuthClientOptions } from '../../config'; import type { OAuthClientOptions } from '../../config';
@ -178,12 +177,6 @@ export const validateOauth = async (options: HandleOAuthCallbackUrlOptions) => {
redirectPath = '/'; redirectPath = '/';
} }
if (!isValidReturnTo(redirectPath)) {
redirectPath = '/';
}
redirectPath = normalizeReturnTo(redirectPath) || '/';
const tokens = await oAuthClient.validateAuthorizationCode( const tokens = await oAuthClient.validateAuthorizationCode(
token_endpoint, token_endpoint,
code, code,

View File

@ -11,7 +11,7 @@ export const validateNumberField = (
const { minValue, maxValue, readOnly, required, numberFormat, fontSize } = fieldMeta || {}; const { minValue, maxValue, readOnly, required, numberFormat, fontSize } = fieldMeta || {};
if (numberFormat && value.length > 0) { if (numberFormat) {
const foundRegex = numberFormatValues.find((item) => item.value === numberFormat)?.regex; const foundRegex = numberFormatValues.find((item) => item.value === numberFormat)?.regex;
if (!foundRegex) { if (!foundRegex) {

View File

@ -1,6 +1,6 @@
import type { EnvelopeItem } from '@prisma/client'; import type { EnvelopeItem } from '@prisma/client';
import { getEnvelopeItemPdfUrl } from '../utils/envelope-download'; import { getEnvelopeDownloadUrl } from '../utils/envelope-download';
import { downloadFile } from './download-file'; import { downloadFile } from './download-file';
type DocumentVersion = 'original' | 'signed'; type DocumentVersion = 'original' | 'signed';
@ -24,8 +24,7 @@ export const downloadPDF = async ({
fileName, fileName,
version = 'signed', version = 'signed',
}: DownloadPDFProps) => { }: DownloadPDFProps) => {
const downloadUrl = getEnvelopeItemPdfUrl({ const downloadUrl = getEnvelopeDownloadUrl({
type: 'download',
envelopeItem: envelopeItem, envelopeItem: envelopeItem,
token, token,
version, version,

View File

@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import type { Field, Recipient } from '@prisma/client'; import type { Recipient } from '@prisma/client';
import { FieldType } from '@prisma/client'; import { FieldType } from '@prisma/client';
import { useFieldArray, useForm } from 'react-hook-form'; import { useFieldArray, useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
@ -63,8 +63,6 @@ type UseEditorFieldsResponse = {
// Selected recipient // Selected recipient
selectedRecipient: Recipient | null; selectedRecipient: Recipient | null;
setSelectedRecipient: (recipientId: number | null) => void; setSelectedRecipient: (recipientId: number | null) => void;
resetForm: (fields?: Field[]) => void;
}; };
export const useEditorFields = ({ export const useEditorFields = ({
@ -74,30 +72,24 @@ export const useEditorFields = ({
const [selectedFieldFormId, setSelectedFieldFormId] = useState<string | null>(null); const [selectedFieldFormId, setSelectedFieldFormId] = useState<string | null>(null);
const [selectedRecipientId, setSelectedRecipientId] = useState<number | null>(null); const [selectedRecipientId, setSelectedRecipientId] = useState<number | null>(null);
const generateDefaultValues = (fields?: Field[]) => {
const formFields = (fields || envelope.fields).map(
(field): TLocalField => ({
id: field.id,
formId: nanoid(),
envelopeItemId: field.envelopeItemId,
page: field.page,
type: field.type,
positionX: Number(field.positionX),
positionY: Number(field.positionY),
width: Number(field.width),
height: Number(field.height),
recipientId: field.recipientId,
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
}),
);
return {
fields: formFields,
};
};
const form = useForm<TEditorFieldsFormSchema>({ const form = useForm<TEditorFieldsFormSchema>({
defaultValues: generateDefaultValues(), defaultValues: {
fields: envelope.fields.map(
(field): TLocalField => ({
id: field.id,
formId: nanoid(),
envelopeItemId: field.envelopeItemId,
page: field.page,
type: field.type,
positionX: Number(field.positionX),
positionY: Number(field.positionY),
width: Number(field.width),
height: Number(field.height),
recipientId: field.recipientId,
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
}),
),
},
resolver: zodResolver(ZEditorFieldsFormSchema), resolver: zodResolver(ZEditorFieldsFormSchema),
}); });
@ -280,10 +272,6 @@ export const useEditorFields = ({
setSelectedRecipientId(foundRecipient?.id ?? null); setSelectedRecipientId(foundRecipient?.id ?? null);
}; };
const resetForm = (fields?: Field[]) => {
form.reset(generateDefaultValues(fields));
};
return { return {
// Core state // Core state
localFields, localFields,
@ -307,8 +295,6 @@ export const useEditorFields = ({
// Selected recipient // Selected recipient
selectedRecipient, selectedRecipient,
setSelectedRecipient, setSelectedRecipient,
resetForm,
}; };
}; };

View File

@ -107,10 +107,6 @@ export function usePageRenderer(renderFunction: RenderFunction) {
stage: stage.current, stage: stage.current,
pageLayer: pageLayer.current, pageLayer: pageLayer.current,
}); });
void document.fonts.ready.then(function () {
pageLayer.current?.batchDraw();
});
}); });
return () => { return () => {

View File

@ -8,7 +8,7 @@ import { AVAILABLE_RECIPIENT_COLORS } from '@documenso/ui/lib/recipient-colors';
import type { TEnvelope } from '../../types/envelope'; import type { TEnvelope } from '../../types/envelope';
import type { FieldRenderMode } from '../../universal/field-renderer/render-field'; import type { FieldRenderMode } from '../../universal/field-renderer/render-field';
import { getEnvelopeItemPdfUrl } from '../../utils/envelope-download'; import { getEnvelopeDownloadUrl } from '../../utils/envelope-download';
type FileData = type FileData =
| { | {
@ -30,8 +30,6 @@ type EnvelopeRenderItem = TEnvelope['envelopeItems'][number];
type EnvelopeRenderProviderValue = { type EnvelopeRenderProviderValue = {
getPdfBuffer: (envelopeItemId: string) => FileData | null; getPdfBuffer: (envelopeItemId: string) => FileData | null;
envelopeItems: EnvelopeRenderItem[]; envelopeItems: EnvelopeRenderItem[];
envelopeStatus: TEnvelope['status'];
envelopeType: TEnvelope['type'];
currentEnvelopeItem: EnvelopeRenderItem | null; currentEnvelopeItem: EnvelopeRenderItem | null;
setCurrentEnvelopeItem: (envelopeItemId: string) => void; setCurrentEnvelopeItem: (envelopeItemId: string) => void;
fields: Field[]; fields: Field[];
@ -46,7 +44,7 @@ type EnvelopeRenderProviderValue = {
interface EnvelopeRenderProviderProps { interface EnvelopeRenderProviderProps {
children: React.ReactNode; children: React.ReactNode;
envelope: Pick<TEnvelope, 'envelopeItems' | 'status' | 'type'>; envelope: Pick<TEnvelope, 'envelopeItems'>;
/** /**
* Optional fields which are passed down to renderers for custom rendering needs. * Optional fields which are passed down to renderers for custom rendering needs.
@ -102,7 +100,7 @@ export const EnvelopeRenderProvider = ({
// Indexed by documentDataId. // Indexed by documentDataId.
const [files, setFiles] = useState<Record<string, FileData>>({}); const [files, setFiles] = useState<Record<string, FileData>>({});
const [currentItem, setCurrentItem] = useState<EnvelopeRenderItem | null>(null); const [currentItem, setItem] = useState<EnvelopeRenderItem | null>(null);
const [renderError, setRenderError] = useState<boolean>(false); const [renderError, setRenderError] = useState<boolean>(false);
@ -126,10 +124,10 @@ export const EnvelopeRenderProvider = ({
} }
try { try {
const downloadUrl = getEnvelopeItemPdfUrl({ const downloadUrl = getEnvelopeDownloadUrl({
type: 'view',
envelopeItem: envelopeItem, envelopeItem: envelopeItem,
token, token,
version: 'signed',
}); });
const blob = await fetch(downloadUrl).then(async (res) => await res.blob()); const blob = await fetch(downloadUrl).then(async (res) => await res.blob());
@ -165,15 +163,11 @@ export const EnvelopeRenderProvider = ({
const setCurrentEnvelopeItem = (envelopeItemId: string) => { const setCurrentEnvelopeItem = (envelopeItemId: string) => {
const foundItem = envelope.envelopeItems.find((item) => item.id === envelopeItemId); const foundItem = envelope.envelopeItems.find((item) => item.id === envelopeItemId);
setCurrentItem(foundItem ?? null); setItem(foundItem ?? null);
}; };
// Set the selected item to the first item if none is set. // Set the selected item to the first item if none is set.
useEffect(() => { useEffect(() => {
if (currentItem && !envelopeItems.some((item) => item.id === currentItem.id)) {
setCurrentItem(null);
}
if (!currentItem && envelopeItems.length > 0) { if (!currentItem && envelopeItems.length > 0) {
setCurrentEnvelopeItem(envelopeItems[0].id); setCurrentEnvelopeItem(envelopeItems[0].id);
} }
@ -209,8 +203,6 @@ export const EnvelopeRenderProvider = ({
value={{ value={{
getPdfBuffer, getPdfBuffer,
envelopeItems, envelopeItems,
envelopeStatus: envelope.status,
envelopeType: envelope.type,
currentEnvelopeItem: currentItem, currentEnvelopeItem: currentItem,
setCurrentEnvelopeItem, setCurrentEnvelopeItem,
fields: fields ?? [], fields: fields ?? [],

View File

@ -32,7 +32,6 @@ export type JobDefinition<Name extends string = string, Schema = any> = {
name: string; name: string;
version: string; version: string;
enabled?: boolean; enabled?: boolean;
optimizeParallelism?: boolean;
trigger: { trigger: {
name: Name; name: Name;
schema?: z.ZodType<Schema>; schema?: z.ZodType<Schema>;

View File

@ -40,7 +40,6 @@ export class InngestJobProvider extends BaseJobProvider {
{ {
id: job.id, id: job.id,
name: job.name, name: job.name,
optimizeParallelism: job.optimizeParallelism ?? false,
}, },
{ {
event: job.trigger.name, event: job.trigger.name,

View File

@ -189,65 +189,29 @@ export const run = async ({
settings, settings,
}); });
// !: The commented out code is our desired implementation but we're seemingly const newDocumentData = await Promise.all(
// !: running into issues with inngest parallelism in production. envelopeItems.map(async (envelopeItem) =>
// !: Until this is resolved we will do this sequentially which is slower but io.runTask(`decorate-and-sign-envelope-item-${envelopeItem.id}`, async () => {
// !: will actually work. const envelopeItemFields = envelope.envelopeItems.find(
// const decoratePromises: Array<Promise<{ oldDocumentDataId: string; newDocumentDataId: string }>> = (item) => item.id === envelopeItem.id,
// []; )?.field;
// for (const envelopeItem of envelopeItems) { if (!envelopeItemFields) {
// const task = io.runTask(`decorate-${envelopeItem.id}`, async () => { throw new Error(`Envelope item fields not found for envelope item ${envelopeItem.id}`);
// const envelopeItemFields = envelope.envelopeItems.find( }
// (item) => item.id === envelopeItem.id,
// )?.field;
// if (!envelopeItemFields) { return decorateAndSignPdf({
// throw new Error(`Envelope item fields not found for envelope item ${envelopeItem.id}`); envelope,
// } envelopeItem,
envelopeItemFields,
// return decorateAndSignPdf({ isRejected,
// envelope, rejectionReason,
// envelopeItem, certificateData,
// envelopeItemFields, auditLogData,
// isRejected, });
// rejectionReason, }),
// certificateData, ),
// auditLogData, );
// });
// });
// decoratePromises.push(task);
// }
// const newDocumentData = await Promise.all(decoratePromises);
// TODO: Remove once parallelization is working
const newDocumentData: Array<{ oldDocumentDataId: string; newDocumentDataId: string }> = [];
for (const envelopeItem of envelopeItems) {
const result = await io.runTask(`decorate-${envelopeItem.id}`, async () => {
const envelopeItemFields = envelope.envelopeItems.find(
(item) => item.id === envelopeItem.id,
)?.field;
if (!envelopeItemFields) {
throw new Error(`Envelope item fields not found for envelope item ${envelopeItem.id}`);
}
return decorateAndSignPdf({
envelope,
envelopeItem,
envelopeItemFields,
isRejected,
rejectionReason,
certificateData,
auditLogData,
});
});
newDocumentData.push(result);
}
const postHog = PostHogServerClient(); const postHog = PostHogServerClient();

View File

@ -18,7 +18,6 @@ export const SEAL_DOCUMENT_JOB_DEFINITION = {
id: SEAL_DOCUMENT_JOB_DEFINITION_ID, id: SEAL_DOCUMENT_JOB_DEFINITION_ID,
name: 'Seal Document', name: 'Seal Document',
version: '1.0.0', version: '1.0.0',
optimizeParallelism: true,
trigger: { trigger: {
name: SEAL_DOCUMENT_JOB_DEFINITION_ID, name: SEAL_DOCUMENT_JOB_DEFINITION_ID,
schema: SEAL_DOCUMENT_JOB_DEFINITION_SCHEMA, schema: SEAL_DOCUMENT_JOB_DEFINITION_SCHEMA,

View File

@ -1,4 +1,4 @@
import type { DocumentData, Envelope, EnvelopeItem, Field } from '@prisma/client'; import type { DocumentData, Envelope, EnvelopeItem } from '@prisma/client';
import { import {
DocumentSigningOrder, DocumentSigningOrder,
DocumentStatus, DocumentStatus,
@ -24,9 +24,7 @@ import {
ZCheckboxFieldMeta, ZCheckboxFieldMeta,
ZDropdownFieldMeta, ZDropdownFieldMeta,
ZFieldAndMetaSchema, ZFieldAndMetaSchema,
ZNumberFieldMeta,
ZRadioFieldMeta, ZRadioFieldMeta,
ZTextFieldMeta,
} from '../../types/field-meta'; } from '../../types/field-meta';
import { import {
ZWebhookDocumentSchema, ZWebhookDocumentSchema,
@ -184,19 +182,80 @@ export const sendDocument = async ({
// Validate and autoinsert fields for V2 envelopes. // Validate and autoinsert fields for V2 envelopes.
if (envelope.internalVersion === 2) { if (envelope.internalVersion === 2) {
for (const unknownField of envelope.fields) { for (const unknownField of envelope.fields) {
const recipient = envelope.recipients.find((r) => r.id === unknownField.recipientId); const parsedField = ZFieldAndMetaSchema.safeParse(unknownField);
if (!recipient) { if (parsedField.error) {
throw new AppError(AppErrorCode.NOT_FOUND, { throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Recipient not found', message: 'One or more fields have invalid metadata. Error: ' + parsedField.error.message,
}); });
} }
const fieldToAutoInsert = extractFieldAutoInsertValues(unknownField); const field = parsedField.data;
const fieldId = unknownField.id;
// Only auto-insert fields if the recipient has not been sent the document yet. if (field.type === FieldType.RADIO) {
if (fieldToAutoInsert && recipient.sendStatus !== SendStatus.SENT) { const { values = [] } = ZRadioFieldMeta.parse(field.fieldMeta);
fieldsToAutoInsert.push(fieldToAutoInsert);
const checkedItemIndex = values.findIndex((value) => value.checked);
if (checkedItemIndex !== -1) {
fieldsToAutoInsert.push({
fieldId,
customText: toRadioCustomText(checkedItemIndex),
});
}
}
if (field.type === FieldType.DROPDOWN) {
const { defaultValue, values = [] } = ZDropdownFieldMeta.parse(field.fieldMeta);
if (defaultValue && values.some((value) => value.value === defaultValue)) {
fieldsToAutoInsert.push({
fieldId,
customText: defaultValue,
});
}
}
if (field.type === FieldType.CHECKBOX) {
const {
values = [],
validationRule,
validationLength,
} = ZCheckboxFieldMeta.parse(field.fieldMeta);
const checkedIndices: number[] = [];
values.forEach((value, i) => {
if (value.checked) {
checkedIndices.push(i);
}
});
let isValid = true;
if (validationRule && validationLength) {
const validation = checkboxValidationSigns.find((sign) => sign.label === validationRule);
if (!validation) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Invalid checkbox validation rule',
});
}
isValid = validateCheckboxLength(
checkedIndices.length,
validation.value,
validationLength,
);
}
if (isValid && checkedIndices.length > 0) {
fieldsToAutoInsert.push({
fieldId,
customText: toCheckboxCustomText(checkedIndices),
});
}
} }
} }
} }
@ -216,7 +275,6 @@ export const sendDocument = async ({
if (envelope.internalVersion === 2) { if (envelope.internalVersion === 2) {
const autoInsertedFields = await Promise.all( const autoInsertedFields = await Promise.all(
fieldsToAutoInsert.map(async (field) => { fieldsToAutoInsert.map(async (field) => {
// Warning: Only auto-insert fields if the recipient has not been sent the document yet.
return await tx.field.update({ return await tx.field.update({
where: { where: {
id: field.fieldId, id: field.fieldId,
@ -329,113 +387,3 @@ const injectFormValuesIntoDocument = async (
}, },
}); });
}; };
/**
* Extracts the auto insertion values for a given field.
*
* If field is not auto insertable, returns `null`.
*/
export const extractFieldAutoInsertValues = (
unknownField: Field,
): { fieldId: number; customText: string } | null => {
const parsedField = ZFieldAndMetaSchema.safeParse(unknownField);
if (parsedField.error) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'One or more fields have invalid metadata. Error: ' + parsedField.error.message,
});
}
const field = parsedField.data;
const fieldId = unknownField.id;
// Auto insert text fields with prefilled values.
if (field.type === FieldType.TEXT) {
const { text } = ZTextFieldMeta.parse(field.fieldMeta);
if (text) {
return {
fieldId,
customText: text,
};
}
}
// Auto insert number fields with prefilled values.
if (field.type === FieldType.NUMBER) {
const { value } = ZNumberFieldMeta.parse(field.fieldMeta);
if (value) {
return {
fieldId,
customText: value,
};
}
}
// Auto insert radio fields with the pre-checked value.
if (field.type === FieldType.RADIO) {
const { values = [] } = ZRadioFieldMeta.parse(field.fieldMeta);
const checkedItemIndex = values.findIndex((value) => value.checked);
if (checkedItemIndex !== -1) {
return {
fieldId,
customText: toRadioCustomText(checkedItemIndex),
};
}
}
// Auto insert dropdown fields with the default value.
if (field.type === FieldType.DROPDOWN) {
const { defaultValue, values = [] } = ZDropdownFieldMeta.parse(field.fieldMeta);
if (defaultValue && values.some((value) => value.value === defaultValue)) {
return {
fieldId,
customText: defaultValue,
};
}
}
// Auto insert checkbox fields with the pre-checked values.
if (field.type === FieldType.CHECKBOX) {
const {
values = [],
validationRule,
validationLength,
} = ZCheckboxFieldMeta.parse(field.fieldMeta);
const checkedIndices: number[] = [];
values.forEach((value, i) => {
if (value.checked) {
checkedIndices.push(i);
}
});
let isValid = true;
if (validationRule && validationLength) {
const validation = checkboxValidationSigns.find((sign) => sign.label === validationRule);
if (!validation) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Invalid checkbox validation rule',
});
}
isValid = validateCheckboxLength(checkedIndices.length, validation.value, validationLength);
}
if (isValid && checkedIndices.length > 0) {
return {
fieldId,
customText: toCheckboxCustomText(checkedIndices),
};
}
}
return null;
};

View File

@ -6,7 +6,6 @@ import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error'; import { AppError, AppErrorCode } from '../../errors/app-error';
import { DocumentAccessAuth, type TDocumentAuthMethods } from '../../types/document-auth'; import { DocumentAccessAuth, type TDocumentAuthMethods } from '../../types/document-auth';
import { extractDocumentAuthMethods } from '../../utils/document-auth'; import { extractDocumentAuthMethods } from '../../utils/document-auth';
import { extractFieldAutoInsertValues } from '../document/send-document';
import { getTeamSettings } from '../team/get-team-settings'; import { getTeamSettings } from '../team/get-team-settings';
import type { EnvelopeForSigningResponse } from './get-envelope-for-recipient-signing'; import type { EnvelopeForSigningResponse } from './get-envelope-for-recipient-signing';
import { ZEnvelopeForSigningResponse } from './get-envelope-for-recipient-signing'; import { ZEnvelopeForSigningResponse } from './get-envelope-for-recipient-signing';
@ -145,19 +144,6 @@ export const getEnvelopeForDirectTemplateSigning = async ({
recipient: { recipient: {
...recipient, ...recipient,
directToken: envelope.directLink?.token || '', directToken: envelope.directLink?.token || '',
fields: recipient.fields.map((field) => {
const autoInsertValue = extractFieldAutoInsertValues(field);
if (!autoInsertValue) {
return field;
}
return {
...field,
inserted: true,
customText: autoInsertValue.customText,
};
}),
}, },
recipientSignature: null, recipientSignature: null,
isRecipientsTurn: true, isRecipientsTurn: true,

View File

@ -11,7 +11,7 @@ import UserSchema from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
import { AppError, AppErrorCode } from '../../errors/app-error'; import { AppError, AppErrorCode } from '../../errors/app-error';
import type { TDocumentAuthMethods } from '../../types/document-auth'; import type { TDocumentAuthMethods } from '../../types/document-auth';
import { ZEnvelopeFieldSchema, ZFieldSchema } from '../../types/field'; import { ZFieldSchema } from '../../types/field';
import { ZRecipientLiteSchema } from '../../types/recipient'; import { ZRecipientLiteSchema } from '../../types/recipient';
import { isRecipientAuthorized } from '../document/is-recipient-authorized'; import { isRecipientAuthorized } from '../document/is-recipient-authorized';
import { getTeamSettings } from '../team/get-team-settings'; import { getTeamSettings } from '../team/get-team-settings';
@ -63,11 +63,9 @@ export const ZEnvelopeForSigningResponse = z.object({
rejectionReason: true, rejectionReason: true,
}) })
.extend({ .extend({
fields: ZEnvelopeFieldSchema.extend({ fields: ZFieldSchema.omit({
signature: SignatureSchema.pick({ documentId: true,
signatureImageAsBase64: true, templateId: true,
typedSignature: true,
}).nullish(),
}).array(), }).array(),
}) })
.array(), .array(),

View File

@ -129,7 +129,7 @@ export const setFieldsForTemplate = async ({
if (field.type === FieldType.NUMBER && field.fieldMeta) { if (field.type === FieldType.NUMBER && field.fieldMeta) {
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta); const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
const errors = validateNumberField( const errors = validateNumberField(
String(numberFieldParsedMeta.value || ''), String(numberFieldParsedMeta.value),
numberFieldParsedMeta, numberFieldParsedMeta,
); );
if (errors.length > 0) { if (errors.length > 0) {

View File

@ -88,13 +88,11 @@ export const addUserToOrganisation = async ({
organisationId, organisationId,
organisationGroups, organisationGroups,
organisationMemberRole, organisationMemberRole,
bypassEmail = false,
}: { }: {
userId: number; userId: number;
organisationId: string; organisationId: string;
organisationGroups: OrganisationGroup[]; organisationGroups: OrganisationGroup[];
organisationMemberRole: OrganisationMemberRole; organisationMemberRole: OrganisationMemberRole;
bypassEmail?: boolean;
}) => { }) => {
const organisationGroupToUse = organisationGroups.find( const organisationGroupToUse = organisationGroups.find(
(group) => (group) =>
@ -124,15 +122,13 @@ export const addUserToOrganisation = async ({
}, },
}); });
if (!bypassEmail) { await jobs.triggerJob({
await jobs.triggerJob({ name: 'send.organisation-member-joined.email',
name: 'send.organisation-member-joined.email', payload: {
payload: { organisationId,
organisationId, memberUserId: userId,
memberUserId: userId, },
}, });
});
}
}, },
{ timeout: 30_000 }, { timeout: 30_000 },
); );

View File

@ -32,8 +32,10 @@ export const insertFieldInPDFV2 = async ({
const stage = new Konva.Stage({ width: pageWidth, height: pageHeight }); const stage = new Konva.Stage({ width: pageWidth, height: pageHeight });
const layer = new Konva.Layer(); const layer = new Konva.Layer();
const insertedFields = fields.filter((field) => field.inserted);
// Render the fields onto the layer. // Render the fields onto the layer.
for (const field of fields) { for (const field of insertedFields) {
renderField({ renderField({
scale: 1, scale: 1,
field: { field: {

View File

@ -215,12 +215,6 @@ export const createDocumentFromDirectTemplate = async ({
const fieldsToProcess = directTemplateRecipient.fields.filter((templateField) => { const fieldsToProcess = directTemplateRecipient.fields.filter((templateField) => {
const signedFieldValue = signedFieldValues.find((value) => value.fieldId === templateField.id); const signedFieldValue = signedFieldValues.find((value) => value.fieldId === templateField.id);
// Custom logic for V2 to include all fields, since v1 excludes read only
// and prefilled fields.
if (directTemplateEnvelope.internalVersion === 2) {
return true;
}
// Include if it's required or has a signed value // Include if it's required or has a signed value
return isRequiredField(templateField) || signedFieldValue !== undefined; return isRequiredField(templateField) || signedFieldValue !== undefined;
}); });
@ -474,28 +468,19 @@ export const createDocumentFromDirectTemplate = async ({
signingOrder: directTemplateRecipient.signingOrder, signingOrder: directTemplateRecipient.signingOrder,
fields: { fields: {
createMany: { createMany: {
data: directTemplateNonSignatureFields.map(({ templateField, customText }) => { data: directTemplateNonSignatureFields.map(({ templateField, customText }) => ({
let inserted = true; envelopeId: createdEnvelope.id,
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[templateField.envelopeItemId],
// Custom logic for V2 to only insert if values exist. type: templateField.type,
if (directTemplateEnvelope.internalVersion === 2) { page: templateField.page,
inserted = customText !== ''; positionX: templateField.positionX,
} positionY: templateField.positionY,
width: templateField.width,
return { height: templateField.height,
envelopeId: createdEnvelope.id, customText: customText ?? '',
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[templateField.envelopeItemId], inserted: true,
type: templateField.type, fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
page: templateField.page, })),
positionX: templateField.positionX,
positionY: templateField.positionY,
width: templateField.width,
height: templateField.height,
customText: customText ?? '',
inserted,
fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
};
}),
}, },
}, },
}, },

File diff suppressed because it is too large Load Diff

View File

@ -53,7 +53,7 @@ msgstr "\"Team Name\" has invited you to sign \"example document\"."
#: apps/remix/app/components/general/envelope-signing/envelope-signer-form.tsx #: apps/remix/app/components/general/envelope-signing/envelope-signer-form.tsx
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx #: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
msgid "(You)" msgid "(You)"
msgstr "(You)" msgstr "(You)"
@ -1076,10 +1076,6 @@ msgstr "Add Placeholder Recipient"
msgid "Add Placeholders" msgid "Add Placeholders"
msgstr "Add Placeholders" msgstr "Add Placeholders"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
msgid "Add Recipients"
msgstr "Add Recipients"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx
#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx #: packages/ui/primitives/document-flow/add-signers.tsx
@ -1128,8 +1124,6 @@ msgstr "Add this URL to your provider's allowed redirect URIs"
msgid "Additional brand information to display at the bottom of emails" msgid "Additional brand information to display at the bottom of emails"
msgstr "Additional brand information to display at the bottom of emails" msgstr "Additional brand information to display at the bottom of emails"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: packages/lib/constants/teams-translations.ts #: packages/lib/constants/teams-translations.ts
#: packages/lib/constants/organisations-translations.ts #: packages/lib/constants/organisations-translations.ts
msgid "Admin" msgid "Admin"
@ -1369,6 +1363,17 @@ msgstr "An error occurred while disabling direct link signing."
msgid "An error occurred while disabling the user." msgid "An error occurred while disabling the user."
msgstr "An error occurred while disabling the user." msgstr "An error occurred while disabling the user."
#: apps/remix/app/components/tables/inbox-table.tsx
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/tables/documents-table-action-button.tsx
#: apps/remix/app/components/general/share-document-download-button.tsx
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
#: apps/remix/app/components/general/document/document-page-view-button.tsx
msgid "An error occurred while downloading your document."
msgstr "An error occurred while downloading your document."
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx #: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
msgid "An error occurred while duplicating template." msgid "An error occurred while duplicating template."
msgstr "An error occurred while duplicating template." msgstr "An error occurred while duplicating template."
@ -1450,10 +1455,6 @@ msgstr "An error occurred while signing as assistant."
msgid "An error occurred while signing the document." msgid "An error occurred while signing the document."
msgstr "An error occurred while signing the document." msgstr "An error occurred while signing the document."
#: apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx
msgid "An error occurred while signing the field."
msgstr "An error occurred while signing the field."
#: apps/remix/app/components/general/billing-plans.tsx #: apps/remix/app/components/general/billing-plans.tsx
#: apps/remix/app/components/general/billing-plans.tsx #: apps/remix/app/components/general/billing-plans.tsx
msgid "An error occurred while trying to create a checkout session." msgid "An error occurred while trying to create a checkout session."
@ -1537,7 +1538,6 @@ msgstr "An unexpected error occurred."
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx #: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx #: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-item-delete-dialog.tsx #: apps/remix/app/components/dialogs/envelope-item-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx #: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-document-delete-dialog.tsx #: apps/remix/app/components/dialogs/admin-document-delete-dialog.tsx
#: apps/remix/app/components/dialogs/account-delete-dialog.tsx #: apps/remix/app/components/dialogs/account-delete-dialog.tsx
@ -1694,7 +1694,7 @@ msgstr "Assist"
msgid "Assist Document" msgid "Assist Document"
msgstr "Assist Document" msgstr "Assist Document"
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
msgid "Assist with signing" msgid "Assist with signing"
msgstr "Assist with signing" msgstr "Assist with signing"
@ -2038,7 +2038,6 @@ msgstr "Can prepare"
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx #: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
#: apps/remix/app/components/dialogs/claim-create-dialog.tsx #: apps/remix/app/components/dialogs/claim-create-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx #: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx #: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
#: packages/ui/primitives/signature-pad/signature-pad-dialog.tsx #: packages/ui/primitives/signature-pad/signature-pad-dialog.tsx
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx #: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
@ -2166,6 +2165,10 @@ msgstr "Clear filters"
msgid "Clear Signature" msgid "Clear Signature"
msgstr "Clear Signature" msgstr "Clear Signature"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
msgid "Click here to add a recipient"
msgstr "Click here to add a recipient"
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx #: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
msgid "Click here to get started" msgid "Click here to get started"
msgstr "Click here to get started" msgstr "Click here to get started"
@ -2188,7 +2191,7 @@ msgstr "Click to copy signing link for sending to recipient"
#: apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx #: apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx #: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx #: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx #: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
msgid "Click to insert field" msgid "Click to insert field"
@ -2231,6 +2234,10 @@ msgstr "Client secret is required"
msgid "Close" msgid "Close"
msgstr "Close" msgstr "Close"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-preview-page.tsx
msgid "Coming soon"
msgstr "Coming soon"
#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx #: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx
msgid "Communication" msgid "Communication"
msgstr "Communication" msgstr "Communication"
@ -2242,8 +2249,8 @@ msgstr "Compare all plans and features in detail"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx #: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
@ -2289,12 +2296,6 @@ msgstr "Completed Documents"
msgid "Completed on {formattedDate}" msgid "Completed on {formattedDate}"
msgstr "Completed on {formattedDate}" msgstr "Completed on {formattedDate}"
#: packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx
#: packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx
#: packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx
msgid "Configuration Error"
msgstr "Configuration Error"
#. placeholder {0}: parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[currentField.type]) #. placeholder {0}: parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[currentField.type])
#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx #: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx
msgid "Configure {0} Field" msgid "Configure {0} Field"
@ -3566,17 +3567,20 @@ msgstr "Domain Name"
msgid "Don't have an account? <0>Sign up</0>" msgid "Don't have an account? <0>Sign up</0>"
msgstr "Don't have an account? <0>Sign up</0>" msgstr "Don't have an account? <0>Sign up</0>"
#: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
#: apps/remix/app/components/tables/organisation-billing-invoices-table.tsx #: apps/remix/app/components/tables/organisation-billing-invoices-table.tsx
#: apps/remix/app/components/tables/inbox-table.tsx #: apps/remix/app/components/tables/inbox-table.tsx
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/tables/documents-table-action-button.tsx #: apps/remix/app/components/tables/documents-table-action-button.tsx
#: apps/remix/app/components/tables/documents-table-action-button.tsx
#: apps/remix/app/components/general/share-document-download-button.tsx
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx #: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
#: apps/remix/app/components/general/document/document-page-view-button.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx #: apps/remix/app/components/general/document/document-page-view-button.tsx
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
#: apps/remix/app/components/forms/2fa/view-recovery-codes-dialog.tsx #: apps/remix/app/components/forms/2fa/view-recovery-codes-dialog.tsx
#: apps/remix/app/components/forms/2fa/enable-authenticator-app-dialog.tsx #: apps/remix/app/components/forms/2fa/enable-authenticator-app-dialog.tsx
#: packages/ui/components/document/document-download-button.tsx
#: packages/email/template-components/template-document-completed.tsx #: packages/email/template-components/template-document-completed.tsx
msgid "Download" msgid "Download"
msgstr "Download" msgstr "Download"
@ -3593,6 +3597,11 @@ msgstr "Download Certificate"
msgid "Download Files" msgid "Download Files"
msgstr "Download Files" msgstr "Download Files"
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
msgid "Download Original"
msgstr "Download Original"
#: apps/remix/app/components/general/envelope-signing/envelope-signer-header.tsx #: apps/remix/app/components/general/envelope-signing/envelope-signer-header.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor.tsx
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx #: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
@ -3743,7 +3752,7 @@ msgstr "Electronic Signature Disclosure"
#: apps/remix/app/components/forms/signin.tsx #: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/profile.tsx #: apps/remix/app/components/forms/profile.tsx
#: apps/remix/app/components/forms/forgot-password.tsx #: apps/remix/app/components/forms/forgot-password.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx #: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx
@ -4014,6 +4023,10 @@ msgstr "Enter your text here"
msgid "Enterprise" msgid "Enterprise"
msgstr "Enterprise" msgstr "Enterprise"
#: packages/ui/primitives/document-upload.tsx
msgid "Envelope (beta)"
msgstr "Envelope (beta)"
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx #: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
msgid "Envelope distributed" msgid "Envelope distributed"
msgstr "Envelope distributed" msgstr "Envelope distributed"
@ -4059,6 +4072,7 @@ msgstr "Envelope updated"
#: apps/remix/app/routes/_authenticated+/settings+/security.linked-accounts.tsx #: apps/remix/app/routes/_authenticated+/settings+/security.linked-accounts.tsx
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
#: apps/remix/app/components/general/verify-email-banner.tsx #: apps/remix/app/components/general/verify-email-banner.tsx
#: apps/remix/app/components/general/template/template-edit-form.tsx #: apps/remix/app/components/general/template/template-edit-form.tsx
@ -4067,7 +4081,6 @@ msgstr "Envelope updated"
#: apps/remix/app/components/general/template/template-edit-form.tsx #: apps/remix/app/components/general/template/template-edit-form.tsx
#: apps/remix/app/components/general/template/template-edit-form.tsx #: apps/remix/app/components/general/template/template-edit-form.tsx
#: apps/remix/app/components/general/template/template-edit-form.tsx #: apps/remix/app/components/general/template/template-edit-form.tsx
#: apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx
#: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx #: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx #: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx #: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
@ -4416,7 +4429,7 @@ msgstr "Free Signature Settings"
#: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx #: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx
#: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/forms/profile.tsx #: apps/remix/app/components/forms/profile.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx #: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
msgid "Full Name" msgid "Full Name"
@ -4561,7 +4574,7 @@ msgstr "has invited you to view this document"
msgid "Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist." msgid "Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist."
msgstr "Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist." msgstr "Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist."
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
msgid "Help complete the document for other signers." msgid "Help complete the document for other signers."
msgstr "Help complete the document for other signers." msgstr "Help complete the document for other signers."
@ -5239,8 +5252,6 @@ msgstr "Manage your passkeys."
msgid "Manage your site settings here" msgid "Manage your site settings here"
msgstr "Manage your site settings here" msgstr "Manage your site settings here"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: packages/lib/constants/teams-translations.ts #: packages/lib/constants/teams-translations.ts
#: packages/lib/constants/organisations-translations.ts #: packages/lib/constants/organisations-translations.ts
msgid "Manager" msgid "Manager"
@ -5289,8 +5300,6 @@ msgstr "Maximum number of uploaded files per envelope allowed"
#: apps/remix/app/components/tables/team-members-table.tsx #: apps/remix/app/components/tables/team-members-table.tsx
#: apps/remix/app/components/tables/admin-organisations-table.tsx #: apps/remix/app/components/tables/admin-organisations-table.tsx
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx #: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: packages/lib/constants/teams-translations.ts #: packages/lib/constants/teams-translations.ts
#: packages/lib/constants/organisations-translations.ts #: packages/lib/constants/organisations-translations.ts
msgid "Member" msgid "Member"
@ -5301,6 +5310,10 @@ msgstr "Member"
msgid "Member Count" msgid "Member Count"
msgstr "Member Count" msgstr "Member Count"
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Member promoted to owner successfully"
msgstr "Member promoted to owner successfully"
#: apps/remix/app/components/tables/organisation-members-table.tsx #: apps/remix/app/components/tables/organisation-members-table.tsx
msgid "Member Since" msgid "Member Since"
msgstr "Member Since" msgstr "Member Since"
@ -5333,10 +5346,6 @@ msgstr "Message <0>(Optional)</0>"
msgid "Min" msgid "Min"
msgstr "Min" msgstr "Min"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
msgid "Missing Recipients"
msgstr "Missing Recipients"
#: apps/remix/app/components/general/template/template-page-view-recipients.tsx #: apps/remix/app/components/general/template/template-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Modify recipients" msgid "Modify recipients"
@ -5475,7 +5484,7 @@ msgstr "New Password"
msgid "New Template" msgid "New Template"
msgstr "New Template" msgstr "New Template"
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx #: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx #: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
@ -5923,15 +5932,9 @@ msgstr "Override organisation settings"
#: apps/remix/app/components/tables/organisation-members-table.tsx #: apps/remix/app/components/tables/organisation-members-table.tsx
#: apps/remix/app/components/tables/admin-organisations-table.tsx #: apps/remix/app/components/tables/admin-organisations-table.tsx
#: apps/remix/app/components/tables/admin-organisations-table.tsx #: apps/remix/app/components/tables/admin-organisations-table.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
msgid "Owner" msgid "Owner"
msgstr "Owner" msgstr "Owner"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
msgid "Ownership transferred to {organisationMemberName}."
msgstr "Ownership transferred to {organisationMemberName}."
#. placeholder {0}: table.getState().pagination.pageIndex + 1 #. placeholder {0}: table.getState().pagination.pageIndex + 1
#. placeholder {1}: table.getPageCount() || 1 #. placeholder {1}: table.getPageCount() || 1
#: packages/ui/primitives/data-table-pagination.tsx #: packages/ui/primitives/data-table-pagination.tsx
@ -6036,7 +6039,6 @@ msgstr "PDF Document"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx
#: apps/remix/app/components/general/document/document-status.tsx #: apps/remix/app/components/general/document/document-status.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: packages/ui/components/document/envelope-recipient-field-tooltip.tsx
#: packages/ui/components/document/document-read-only-fields.tsx #: packages/ui/components/document/document-read-only-fields.tsx
#: packages/lib/constants/document.ts #: packages/lib/constants/document.ts
msgid "Pending" msgid "Pending"
@ -6374,6 +6376,10 @@ msgstr "Profile updated"
msgid "Progress" msgid "Progress"
msgstr "Progress" msgstr "Progress"
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Promote to owner"
msgstr "Promote to owner"
#: apps/remix/app/routes/_authenticated+/settings+/security.linked-accounts.tsx #: apps/remix/app/routes/_authenticated+/settings+/security.linked-accounts.tsx
msgid "Provider" msgid "Provider"
msgstr "Provider" msgstr "Provider"
@ -6874,7 +6880,6 @@ msgstr "Right"
#: apps/remix/app/components/dialogs/team-member-update-dialog.tsx #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-update-dialog.tsx #: apps/remix/app/components/dialogs/team-group-update-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
msgid "Role" msgid "Role"
msgstr "Role" msgstr "Role"
@ -7334,7 +7339,7 @@ msgstr "Sign as<0>{0} <1>({1})</1></0>"
msgid "Sign Checkbox Field" msgid "Sign Checkbox Field"
msgstr "Sign Checkbox Field" msgstr "Sign Checkbox Field"
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx #: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
msgid "Sign document" msgid "Sign document"
@ -7402,7 +7407,7 @@ msgstr "Sign Signature Field"
msgid "Sign Text Field" msgid "Sign Text Field"
msgstr "Sign Text Field" msgstr "Sign Text Field"
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx #: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
msgid "Sign the document to complete the process." msgid "Sign the document to complete the process."
@ -7444,7 +7449,7 @@ msgstr "Sign your initials into the field"
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx #: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx #: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx
#: apps/remix/app/components/forms/profile.tsx #: apps/remix/app/components/forms/profile.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx #: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
#: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx
@ -7478,10 +7483,13 @@ msgstr "Signature types"
msgid "Signatures Collected" msgid "Signatures Collected"
msgstr "Signatures Collected" msgstr "Signatures Collected"
#: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
msgid "Signatures will appear once the document has been completed"
msgstr "Signatures will appear once the document has been completed"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx #: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx #: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
#: packages/ui/components/document/envelope-recipient-field-tooltip.tsx
#: packages/ui/components/document/document-read-only-fields.tsx #: packages/ui/components/document/document-read-only-fields.tsx
msgid "Signed" msgid "Signed"
msgstr "Signed" msgstr "Signed"
@ -7527,7 +7535,7 @@ msgstr "Signing certificate provided by"
msgid "Signing Complete!" msgid "Signing Complete!"
msgstr "Signing Complete!" msgstr "Signing Complete!"
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
msgid "Signing for" msgid "Signing for"
msgstr "Signing for" msgstr "Signing for"
@ -7587,6 +7595,11 @@ msgstr "Some signers have not been assigned a signature field. Please assign at
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
#: apps/remix/app/components/tables/organisation-member-invites-table.tsx #: apps/remix/app/components/tables/organisation-member-invites-table.tsx
#: apps/remix/app/components/tables/organisation-member-invites-table.tsx #: apps/remix/app/components/tables/organisation-member-invites-table.tsx
#: apps/remix/app/components/tables/inbox-table.tsx
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/tables/documents-table-action-button.tsx
#: apps/remix/app/components/general/share-document-download-button.tsx
#: apps/remix/app/components/general/billing-plans.tsx #: apps/remix/app/components/general/billing-plans.tsx
#: apps/remix/app/components/general/billing-plans.tsx #: apps/remix/app/components/general/billing-plans.tsx
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx #: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
@ -7597,14 +7610,16 @@ msgstr "Some signers have not been assigned a signature field. Please assign at
#: apps/remix/app/components/general/organisations/organisation-billing-portal-button.tsx #: apps/remix/app/components/general/organisations/organisation-billing-portal-button.tsx
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx #: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
#: apps/remix/app/components/general/envelope-signing/envelope-signing-complete-dialog.tsx #: apps/remix/app/components/general/envelope-signing/envelope-signing-complete-dialog.tsx
#: apps/remix/app/components/general/envelope-signing/envelope-signing-complete-dialog.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-page.tsx #: apps/remix/app/components/general/document-signing/document-signing-auth-page.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx #: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
#: apps/remix/app/components/general/document/document-page-view-button.tsx
#: apps/remix/app/components/general/document/document-certificate-download-button.tsx #: apps/remix/app/components/general/document/document-certificate-download-button.tsx
#: apps/remix/app/components/general/document/document-audit-log-download-button.tsx #: apps/remix/app/components/general/document/document-audit-log-download-button.tsx
#: apps/remix/app/components/general/direct-template/direct-template-signing-auth-page.tsx #: apps/remix/app/components/general/direct-template/direct-template-signing-auth-page.tsx
#: apps/remix/app/components/general/direct-template/direct-template-page.tsx #: apps/remix/app/components/general/direct-template/direct-template-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
@ -7643,11 +7658,6 @@ msgstr "Something went wrong while loading the document."
msgid "Something went wrong while loading your passkeys." msgid "Something went wrong while loading your passkeys."
msgstr "Something went wrong while loading your passkeys." msgstr "Something went wrong while loading your passkeys."
#: packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx
#: packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx
msgid "Something went wrong while rendering the document, some fields may be missing or corrupted."
msgstr "Something went wrong while rendering the document, some fields may be missing or corrupted."
#: apps/remix/app/components/general/verify-email-banner.tsx #: apps/remix/app/components/general/verify-email-banner.tsx
msgid "Something went wrong while sending the confirmation email." msgid "Something went wrong while sending the confirmation email."
msgstr "Something went wrong while sending the confirmation email." msgstr "Something went wrong while sending the confirmation email."
@ -7798,6 +7808,7 @@ msgstr "Subscription invalid"
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx #: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
#: apps/remix/app/components/tables/organisation-member-invites-table.tsx #: apps/remix/app/components/tables/organisation-member-invites-table.tsx
@ -7842,7 +7853,6 @@ msgstr "Subscription invalid"
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx #: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx #: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-item-delete-dialog.tsx #: apps/remix/app/components/dialogs/envelope-item-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx #: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
msgid "Success" msgid "Success"
msgstr "Success" msgstr "Success"
@ -8513,10 +8523,6 @@ msgstr "The token you have used to reset your password is either expired or it n
msgid "The two-factor authentication code provided is incorrect" msgid "The two-factor authentication code provided is incorrect"
msgstr "The two-factor authentication code provided is incorrect" msgstr "The two-factor authentication code provided is incorrect"
#: apps/remix/app/components/forms/editor/editor-field-signature-form.tsx
msgid "The typed signature font size"
msgstr "The typed signature font size"
#: packages/ui/components/document/document-signature-settings-tooltip.tsx #: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "The types of signatures that recipients are allowed to use when signing the document." msgid "The types of signatures that recipients are allowed to use when signing the document."
msgstr "The types of signatures that recipients are allowed to use when signing the document." msgstr "The types of signatures that recipients are allowed to use when signing the document."
@ -8570,10 +8576,6 @@ msgstr "There are no completed documents yet. Documents that you have created or
msgid "There was an error uploading your file. Please try again." msgid "There was an error uploading your file. Please try again."
msgstr "There was an error uploading your file. Please try again." msgstr "There was an error uploading your file. Please try again."
#: packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx
msgid "There was an issue rendering some fields, please review the fields and try again."
msgstr "There was an issue rendering some fields, please review the fields and try again."
#: packages/ui/components/document/document-global-auth-action-select.tsx #: packages/ui/components/document/document-global-auth-action-select.tsx
msgid "These can be overriden by setting the authentication requirements directly on each recipient in the next step. Multiple methods can be selected." msgid "These can be overriden by setting the authentication requirements directly on each recipient in the next step. Multiple methods can be selected."
msgstr "These can be overriden by setting the authentication requirements directly on each recipient in the next step. Multiple methods can be selected." msgstr "These can be overriden by setting the authentication requirements directly on each recipient in the next step. Multiple methods can be selected."
@ -8722,6 +8724,10 @@ msgstr "This envelope could not be distributed at this time. Please try again."
msgid "This envelope could not be resent at this time. Please try again." msgid "This envelope could not be resent at this time. Please try again."
msgstr "This envelope could not be resent at this time. Please try again." msgstr "This envelope could not be resent at this time. Please try again."
#: apps/remix/app/components/general/envelope-editor/envelope-editor-preview-page.tsx
msgid "This feature is coming soon"
msgstr "This feature is coming soon"
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them." msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them."
msgstr "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them." msgstr "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them."
@ -9193,7 +9199,6 @@ msgstr "Untitled Group"
#: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx #: apps/remix/app/components/dialogs/folder-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Update" msgid "Update"
@ -9230,7 +9235,6 @@ msgstr "Update organisation"
#: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
msgid "Update organisation member" msgid "Update organisation member"
msgstr "Update organisation member" msgstr "Update organisation member"
@ -9251,11 +9255,9 @@ msgstr "Update profile"
msgid "Update Recipient" msgid "Update Recipient"
msgstr "Update Recipient" msgstr "Update Recipient"
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/components/tables/team-members-table.tsx #: apps/remix/app/components/tables/team-members-table.tsx
#: apps/remix/app/components/tables/team-groups-table.tsx #: apps/remix/app/components/tables/team-groups-table.tsx
#: apps/remix/app/components/tables/organisation-members-table.tsx #: apps/remix/app/components/tables/organisation-members-table.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
msgid "Update role" msgid "Update role"
msgstr "Update role" msgstr "Update role"
@ -9294,10 +9296,6 @@ msgstr "Update user"
msgid "Update webhook" msgid "Update webhook"
msgstr "Update webhook" msgstr "Update webhook"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
msgid "Updated {organisationMemberName} to {roleLabel}."
msgstr "Updated {organisationMemberName} to {roleLabel}."
#: apps/remix/app/components/forms/password.tsx #: apps/remix/app/components/forms/password.tsx
msgid "Updating password..." msgid "Updating password..."
msgstr "Updating password..." msgstr "Updating password..."
@ -9368,10 +9366,6 @@ msgstr "Upload Document"
msgid "Upload documents and add recipients" msgid "Upload documents and add recipients"
msgstr "Upload documents and add recipients" msgstr "Upload documents and add recipients"
#: packages/ui/primitives/document-upload.tsx
msgid "Upload Envelope"
msgstr "Upload Envelope"
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx #: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
#: apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx
#: apps/remix/app/components/general/document/envelope-upload-button.tsx #: apps/remix/app/components/general/document/envelope-upload-button.tsx
@ -9668,6 +9662,10 @@ msgstr "View more"
msgid "View next document" msgid "View next document"
msgstr "View next document" msgstr "View next document"
#: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
msgid "View Original Document"
msgstr "View Original Document"
#: apps/remix/app/components/tables/admin-organisations-table.tsx #: apps/remix/app/components/tables/admin-organisations-table.tsx
msgid "View owner" msgid "View owner"
msgstr "View owner" msgstr "View owner"
@ -9782,6 +9780,10 @@ msgstr "We are unable to update this passkey at the moment. Please try again lat
msgid "We couldn't create a Stripe customer. Please try again." msgid "We couldn't create a Stripe customer. Please try again."
msgstr "We couldn't create a Stripe customer. Please try again." msgstr "We couldn't create a Stripe customer. Please try again."
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "We couldn't promote the member to owner. Please try again."
msgstr "We couldn't promote the member to owner. Please try again."
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups.$id.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups.$id.tsx
msgid "We couldn't update the group. Please try again." msgid "We couldn't update the group. Please try again."
msgstr "We couldn't update the group. Please try again." msgstr "We couldn't update the group. Please try again."
@ -9944,7 +9946,6 @@ msgid "We encountered an unknown error while attempting to update the template.
msgstr "We encountered an unknown error while attempting to update the template. Please try again later." msgstr "We encountered an unknown error while attempting to update the template. Please try again later."
#: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
msgid "We encountered an unknown error while attempting to update this organisation member. Please try again later." msgid "We encountered an unknown error while attempting to update this organisation member. Please try again later."
msgstr "We encountered an unknown error while attempting to update this organisation member. Please try again later." msgstr "We encountered an unknown error while attempting to update this organisation member. Please try again later."
@ -10017,10 +10018,9 @@ msgstr "We were unable to set your public profile to public. Please try again."
msgid "We were unable to setup two-factor authentication for your account. Please ensure that you have entered your code correctly and try again." msgid "We were unable to setup two-factor authentication for your account. Please ensure that you have entered your code correctly and try again."
msgstr "We were unable to setup two-factor authentication for your account. Please ensure that you have entered your code correctly and try again." msgstr "We were unable to setup two-factor authentication for your account. Please ensure that you have entered your code correctly and try again."
#: apps/remix/app/components/general/envelope-signing/envelope-signing-complete-dialog.tsx
#: apps/remix/app/components/general/envelope-signing/envelope-signing-complete-dialog.tsx #: apps/remix/app/components/general/envelope-signing/envelope-signing-complete-dialog.tsx
#: apps/remix/app/components/general/direct-template/direct-template-page.tsx #: apps/remix/app/components/general/direct-template/direct-template-page.tsx
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx #: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx #: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
msgid "We were unable to submit this document at this time. Please try again later." msgid "We were unable to submit this document at this time. Please try again later."
msgstr "We were unable to submit this document at this time. Please try again later." msgstr "We were unable to submit this document at this time. Please try again later."
@ -10322,7 +10322,6 @@ msgid "You are currently updating <0>{memberName}.</0>"
msgstr "You are currently updating <0>{memberName}.</0>" msgstr "You are currently updating <0>{memberName}.</0>"
#: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
msgid "You are currently updating <0>{organisationMemberName}.</0>" msgid "You are currently updating <0>{organisationMemberName}.</0>"
msgstr "You are currently updating <0>{organisationMemberName}.</0>" msgstr "You are currently updating <0>{organisationMemberName}.</0>"
@ -10496,7 +10495,7 @@ msgid "You have been invited to join the following organisation"
msgstr "You have been invited to join the following organisation" msgstr "You have been invited to join the following organisation"
#: packages/lib/server-only/recipient/set-document-recipients.ts #: packages/lib/server-only/recipient/set-document-recipients.ts
#: packages/lib/server-only/recipient/delete-envelope-recipient.ts #: packages/lib/server-only/recipient/delete-document-recipient.ts
msgid "You have been removed from a document" msgid "You have been removed from a document"
msgstr "You have been removed from a document" msgstr "You have been removed from a document"
@ -10665,7 +10664,6 @@ msgstr "You need to be an admin to manage API tokens."
msgid "You need to be logged in as <0>{email}</0> to view this page." msgid "You need to be logged in as <0>{email}</0> to view this page."
msgstr "You need to be logged in as <0>{email}</0> to view this page." msgstr "You need to be logged in as <0>{email}</0> to view this page."
#: apps/remix/app/components/general/document-signing/document-signing-auth-page.tsx
#: apps/remix/app/components/general/direct-template/direct-template-signing-auth-page.tsx #: apps/remix/app/components/general/direct-template/direct-template-signing-auth-page.tsx
msgid "You need to be logged in to view this page." msgid "You need to be logged in to view this page."
msgstr "You need to be logged in to view this page." msgstr "You need to be logged in to view this page."

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,46 +1,9 @@
import { msg } from '@lingui/core/macro';
import { FieldType } from '@prisma/client'; import { FieldType } from '@prisma/client';
import { z } from 'zod'; import { z } from 'zod';
import { DEFAULT_SIGNATURE_TEXT_FONT_SIZE } from '../constants/pdf'; import { DEFAULT_SIGNATURE_TEXT_FONT_SIZE } from '../constants/pdf';
export const FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN = 'middle'; export const DEFAULT_FIELD_FONT_SIZE = 14;
export const FIELD_DEFAULT_GENERIC_ALIGN = 'left';
export const FIELD_DEFAULT_LINE_HEIGHT = 1;
export const FIELD_DEFAULT_LETTER_SPACING = 0;
export const FIELD_MIN_LINE_HEIGHT = 1;
export const FIELD_MAX_LINE_HEIGHT = 10;
export const FIELD_MIN_LETTER_SPACING = 0;
export const FIELD_MAX_LETTER_SPACING = 100;
export const DEFAULT_FIELD_FONT_SIZE = 12;
/**
* Grouped field types that use the same generic text rendering function.
*/
export type GenericTextFieldTypeMetas =
| TInitialsFieldMeta
| TNameFieldMeta
| TEmailFieldMeta
| TDateFieldMeta
| TTextFieldMeta
| TNumberFieldMeta;
const ZFieldMetaLineHeight = z.coerce
.number()
.min(FIELD_MIN_LINE_HEIGHT)
.max(FIELD_MAX_LINE_HEIGHT)
.describe('The line height of the text');
const ZFieldMetaLetterSpacing = z.coerce
.number()
.min(FIELD_MIN_LETTER_SPACING)
.max(FIELD_MAX_LETTER_SPACING)
.describe('The spacing between each character');
const ZFieldMetaVerticalAlign = z
.enum(['top', 'middle', 'bottom'])
.describe('The vertical alignment of the text');
export const ZBaseFieldMeta = z.object({ export const ZBaseFieldMeta = z.object({
label: z.string().optional(), label: z.string().optional(),
@ -87,14 +50,8 @@ export type TDateFieldMeta = z.infer<typeof ZDateFieldMeta>;
export const ZTextFieldMeta = ZBaseFieldMeta.extend({ export const ZTextFieldMeta = ZBaseFieldMeta.extend({
type: z.literal('text'), type: z.literal('text'),
text: z.string().optional(), text: z.string().optional(),
characterLimit: z.coerce characterLimit: z.number().optional(),
.number({ invalid_type_error: msg`Value must be a number`.id })
.min(0)
.optional(),
textAlign: ZFieldTextAlignSchema.optional(), textAlign: ZFieldTextAlignSchema.optional(),
lineHeight: ZFieldMetaLineHeight.nullish(),
letterSpacing: ZFieldMetaLetterSpacing.nullish(),
verticalAlign: ZFieldMetaVerticalAlign.nullish(),
}); });
export type TTextFieldMeta = z.infer<typeof ZTextFieldMeta>; export type TTextFieldMeta = z.infer<typeof ZTextFieldMeta>;
@ -106,9 +63,6 @@ export const ZNumberFieldMeta = ZBaseFieldMeta.extend({
minValue: z.coerce.number().nullish(), minValue: z.coerce.number().nullish(),
maxValue: z.coerce.number().nullish(), maxValue: z.coerce.number().nullish(),
textAlign: ZFieldTextAlignSchema.optional(), textAlign: ZFieldTextAlignSchema.optional(),
lineHeight: ZFieldMetaLineHeight.nullish(),
letterSpacing: ZFieldMetaLetterSpacing.nullish(),
verticalAlign: ZFieldMetaVerticalAlign.nullish(),
}); });
export type TNumberFieldMeta = z.infer<typeof ZNumberFieldMeta>; export type TNumberFieldMeta = z.infer<typeof ZNumberFieldMeta>;

View File

@ -26,7 +26,7 @@ export const renderCheckboxFieldElement = (
field: FieldToRender, field: FieldToRender,
options: RenderFieldElementOptions, options: RenderFieldElementOptions,
) => { ) => {
const { pageWidth, pageHeight, pageLayer, mode, color } = options; const { pageWidth, pageHeight, pageLayer, mode } = options;
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight); const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
@ -210,9 +210,7 @@ export const renderCheckboxFieldElement = (
fieldGroup.add(text); fieldGroup.add(text);
}); });
if (color !== 'readOnly' && mode !== 'export') { createFieldHoverInteraction({ fieldGroup, fieldRect, options });
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
}
return { return {
fieldGroup, fieldGroup,

View File

@ -50,7 +50,7 @@ export const renderDropdownFieldElement = (
field: FieldToRender, field: FieldToRender,
options: RenderFieldElementOptions, options: RenderFieldElementOptions,
) => { ) => {
const { pageWidth, pageHeight, pageLayer, mode, translations, color } = options; const { pageWidth, pageHeight, pageLayer, mode, translations } = options;
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight); const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
@ -74,21 +74,6 @@ export const renderDropdownFieldElement = (
const fontSize = dropdownMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE; const fontSize = dropdownMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
// Don't show any labels when exporting.
if (mode === 'export') {
selectedValue = '';
}
// Render the default value if readonly.
if (
dropdownMeta?.readOnly &&
dropdownMeta.defaultValue &&
dropdownMeta.values &&
dropdownMeta.values.some((value) => value.value === dropdownMeta.defaultValue)
) {
selectedValue = dropdownMeta.defaultValue;
}
if (field.inserted) { if (field.inserted) {
selectedValue = field.customText; selectedValue = field.customText;
} }
@ -181,9 +166,7 @@ export const renderDropdownFieldElement = (
pageLayer.batchDraw(); pageLayer.batchDraw();
}); });
if (color !== 'readOnly' && mode !== 'export') { createFieldHoverInteraction({ fieldGroup, fieldRect, options });
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
}
return { return {
fieldGroup, fieldGroup,

View File

@ -77,7 +77,6 @@ export const renderField = ({
scale, scale,
}; };
// If the generic text field element array changes, update the `GenericTextFieldTypeMetas` type
return match(field.type) return match(field.type)
.with( .with(
FieldType.INITIALS, FieldType.INITIALS,

View File

@ -1,13 +1,7 @@
import Konva from 'konva'; import Konva from 'konva';
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf'; import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
import type { GenericTextFieldTypeMetas } from '../../types/field-meta'; import type { TTextFieldMeta } from '../../types/field-meta';
import {
FIELD_DEFAULT_GENERIC_ALIGN,
FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN,
FIELD_DEFAULT_LETTER_SPACING,
FIELD_DEFAULT_LINE_HEIGHT,
} from '../../types/field-meta';
import { import {
createFieldHoverInteraction, createFieldHoverInteraction,
konvaTextFill, konvaTextFill,
@ -18,14 +12,14 @@ import {
import type { FieldToRender, RenderFieldElementOptions } from './field-renderer'; import type { FieldToRender, RenderFieldElementOptions } from './field-renderer';
import { calculateFieldPosition } from './field-renderer'; import { calculateFieldPosition } from './field-renderer';
const DEFAULT_TEXT_X_PADDING = 6; const DEFAULT_TEXT_ALIGN = 'left';
const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOptions): Konva.Text => { const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOptions): Konva.Text => {
const { pageWidth, pageHeight, mode = 'edit', pageLayer, translations } = options; const { pageWidth, pageHeight, mode = 'edit', pageLayer, translations } = options;
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight); const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
const fieldMeta = field.fieldMeta as GenericTextFieldTypeMetas | undefined; const textMeta = field.fieldMeta as TTextFieldMeta | undefined;
const fieldTypeName = translations?.[field.type] || field.type; const fieldTypeName = translations?.[field.type] || field.type;
@ -39,77 +33,53 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
// Calculate text positioning based on alignment // Calculate text positioning based on alignment
const textX = 0; const textX = 0;
const textY = 0; const textY = 0;
const textFontSize = fieldMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE; let textAlign: 'left' | 'center' | 'right' = textMeta?.textAlign || DEFAULT_TEXT_ALIGN;
const textVerticalAlign: 'top' | 'middle' | 'bottom' = 'middle';
const textFontSize = textMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
const textPadding = 10;
// By default, render the field name or label centered let textToRender: string = fieldTypeName;
let textToRender: string = fieldMeta?.label || fieldTypeName;
let textAlign: 'left' | 'center' | 'right' = 'center';
let textVerticalAlign: 'top' | 'middle' | 'bottom' = FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN;
let textLineHeight = FIELD_DEFAULT_LINE_HEIGHT;
let textLetterSpacing = FIELD_DEFAULT_LETTER_SPACING;
// Render default values for text/number if provided for editing mode. // Handle edit mode.
if (mode === 'edit' && (fieldMeta?.type === 'text' || fieldMeta?.type === 'number')) { if (mode === 'edit') {
const value = fieldMeta?.type === 'text' ? fieldMeta.text : fieldMeta.value; if (textMeta?.text) {
textToRender = textMeta.text;
if (value) { } else {
textToRender = value; // Show field name which is centered for the edit mode if no label/text is avaliable.
textToRender = textMeta?.label || fieldTypeName;
textVerticalAlign = fieldMeta.verticalAlign || FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN; textAlign = 'center';
textAlign = fieldMeta.textAlign || FIELD_DEFAULT_GENERIC_ALIGN;
textLetterSpacing = fieldMeta.letterSpacing || FIELD_DEFAULT_LETTER_SPACING;
textLineHeight = fieldMeta.lineHeight || FIELD_DEFAULT_LINE_HEIGHT;
} }
} }
// Default to blank for export mode since we want to ensure we don't show // Handle sign mode.
// any placeholder text or labels unless actually it's inserted. if (mode === 'sign' || mode === 'export') {
if (mode === 'export') { if (!field.inserted) {
textToRender = ''; if (textMeta?.text) {
} textToRender = textMeta.text;
} else if (mode === 'sign') {
// Only show the field name in sign mode if no text/label is avaliable.
textToRender = textMeta?.label || fieldTypeName;
textAlign = 'center';
}
}
// Fallback render readonly fields if prefilled value exists. if (field.inserted) {
if (field?.fieldMeta?.readOnly && (fieldMeta?.type === 'text' || fieldMeta?.type === 'number')) { textToRender = field.customText;
const value = fieldMeta?.type === 'text' ? fieldMeta.text : fieldMeta.value;
if (value) {
textToRender = value;
textVerticalAlign = fieldMeta.verticalAlign || FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN;
textAlign = fieldMeta.textAlign || FIELD_DEFAULT_GENERIC_ALIGN;
textLetterSpacing = fieldMeta.letterSpacing || FIELD_DEFAULT_LETTER_SPACING;
textLineHeight = fieldMeta.lineHeight || FIELD_DEFAULT_LINE_HEIGHT;
} }
} }
// Override everything with value if it's inserted.
if (field.inserted) {
textToRender = field.customText;
textAlign = fieldMeta?.textAlign || FIELD_DEFAULT_GENERIC_ALIGN;
if (fieldMeta?.type === 'text' || fieldMeta?.type === 'number') {
textVerticalAlign = fieldMeta.verticalAlign || FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN;
textLetterSpacing = fieldMeta.letterSpacing || FIELD_DEFAULT_LETTER_SPACING;
textLineHeight = fieldMeta.lineHeight || FIELD_DEFAULT_LINE_HEIGHT;
}
}
// Note: Do not use native text padding since it's uniform.
// We only want to have padding on the left and right hand sides.
fieldText.setAttrs({ fieldText.setAttrs({
x: textX + DEFAULT_TEXT_X_PADDING, x: textX,
y: textY, y: textY,
verticalAlign: textVerticalAlign, verticalAlign: textVerticalAlign,
wrap: 'word', wrap: 'word',
padding: textPadding,
text: textToRender, text: textToRender,
fontSize: textFontSize, fontSize: textFontSize,
align: textAlign,
lineHeight: textLineHeight,
letterSpacing: textLetterSpacing,
fontFamily: konvaTextFontFamily, fontFamily: konvaTextFontFamily,
fill: konvaTextFill, fill: konvaTextFill,
width: fieldWidth - DEFAULT_TEXT_X_PADDING * 2, align: textAlign,
width: fieldWidth,
height: fieldHeight, height: fieldHeight,
} satisfies Partial<Konva.TextConfig>); } satisfies Partial<Konva.TextConfig>);
@ -120,7 +90,7 @@ export const renderGenericTextFieldElement = (
field: FieldToRender, field: FieldToRender,
options: RenderFieldElementOptions, options: RenderFieldElementOptions,
) => { ) => {
const { mode = 'edit', pageLayer, color } = options; const { mode = 'edit', pageLayer } = options;
const isFirstRender = !pageLayer.findOne(`#${field.renderId}`); const isFirstRender = !pageLayer.findOne(`#${field.renderId}`);
@ -155,7 +125,7 @@ export const renderGenericTextFieldElement = (
const rectHeight = fieldRect.height() * groupScaleY; const rectHeight = fieldRect.height() * groupScaleY;
// Update text dimensions // Update text dimensions
fieldText.width(rectWidth - DEFAULT_TEXT_X_PADDING * 2); fieldText.width(rectWidth);
fieldText.height(rectHeight); fieldText.height(rectHeight);
// Force Konva to recalculate text layout // Force Konva to recalculate text layout
@ -173,7 +143,7 @@ export const renderGenericTextFieldElement = (
const rectHeight = fieldRect.height(); const rectHeight = fieldRect.height();
// Update text dimensions // Update text dimensions
fieldText.width(rectWidth - DEFAULT_TEXT_X_PADDING * 2); fieldText.width(rectWidth); // Account for padding
fieldText.height(rectHeight); fieldText.height(rectHeight);
// Force Konva to recalculate text layout // Force Konva to recalculate text layout
@ -188,9 +158,7 @@ export const renderGenericTextFieldElement = (
fieldRect.opacity(0); fieldRect.opacity(0);
} }
if (color !== 'readOnly' && mode !== 'export') { createFieldHoverInteraction({ fieldGroup, fieldRect, options });
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
}
return { return {
fieldGroup, fieldGroup,

View File

@ -25,7 +25,7 @@ export const renderRadioFieldElement = (
field: FieldToRender, field: FieldToRender,
options: RenderFieldElementOptions, options: RenderFieldElementOptions,
) => { ) => {
const { pageWidth, pageHeight, pageLayer, mode, color } = options; const { pageWidth, pageHeight, pageLayer, mode } = options;
const radioMeta: TRadioFieldMeta | null = (field.fieldMeta as TRadioFieldMeta) || null; const radioMeta: TRadioFieldMeta | null = (field.fieldMeta as TRadioFieldMeta) || null;
const radioValues = radioMeta?.values || []; const radioValues = radioMeta?.values || [];
@ -195,9 +195,7 @@ export const renderRadioFieldElement = (
fieldGroup.add(text); fieldGroup.add(text);
}); });
if (color !== 'readOnly' && mode !== 'export') { createFieldHoverInteraction({ fieldGroup, fieldRect, options });
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
}
return { return {
fieldGroup, fieldGroup,

View File

@ -142,7 +142,7 @@ export const renderSignatureFieldElement = (
field: FieldToRender, field: FieldToRender,
options: RenderFieldElementOptions, options: RenderFieldElementOptions,
) => { ) => {
const { mode = 'edit', pageLayer, color } = options; const { mode = 'edit', pageLayer } = options;
const isFirstRender = !pageLayer.findOne(`#${field.renderId}`); const isFirstRender = !pageLayer.findOne(`#${field.renderId}`);
@ -211,9 +211,7 @@ export const renderSignatureFieldElement = (
fieldRect.opacity(0); fieldRect.opacity(0);
} }
if (color !== 'readOnly' && mode !== 'export') { createFieldHoverInteraction({ fieldGroup, fieldRect, options });
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
}
return { return {
fieldGroup, fieldGroup,

View File

@ -6,7 +6,7 @@ import { env } from '@documenso/lib/utils/env';
import type { import type {
TGetPresignedPostUrlResponse, TGetPresignedPostUrlResponse,
TUploadPdfResponse, TUploadPdfResponse,
} from '@documenso/remix/server/api/files/files.types'; } from '@documenso/remix/server/api/files.types';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { AppError } from '../../errors/app-error'; import { AppError } from '../../errors/app-error';

View File

@ -2,33 +2,18 @@ import type { EnvelopeItem } from '@prisma/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
export type EnvelopeItemPdfUrlOptions = export type EnvelopeDownloadUrlOptions = {
| { envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
type: 'download'; token: string | undefined;
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>; version: 'original' | 'signed';
token: string | undefined; };
version: 'original' | 'signed';
}
| {
type: 'view';
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
token: string | undefined;
};
export const getEnvelopeItemPdfUrl = (options: EnvelopeItemPdfUrlOptions) => { export const getEnvelopeDownloadUrl = (options: EnvelopeDownloadUrlOptions) => {
const { envelopeItem, token, type } = options; const { envelopeItem, token, version } = options;
const { id, envelopeId } = envelopeItem; const { id, envelopeId } = envelopeItem;
if (type === 'download') {
const version = options.version;
return token
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}/download/${version}`
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}/download/${version}`;
}
return token return token
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}` ? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}/download/${version}`
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}`; : `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}/download/${version}`;
}; };

View File

@ -102,7 +102,7 @@ export const extractFieldInsertionValues = ({
} }
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta); const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
const errors = validateNumberField(fieldValue.value, numberFieldParsedMeta, true); const errors = validateNumberField(fieldValue.value.toString(), numberFieldParsedMeta, true);
if (errors.length > 0) { if (errors.length > 0) {
throw new AppError(AppErrorCode.INVALID_BODY, { throw new AppError(AppErrorCode.INVALID_BODY, {
@ -111,7 +111,7 @@ export const extractFieldInsertionValues = ({
} }
return { return {
customText: fieldValue.value, customText: fieldValue.value.toString(),
inserted: true, inserted: true,
}; };
}) })

View File

@ -1,11 +1,5 @@
import type { Envelope, Recipient } from '@prisma/client'; import type { Envelope, Recipient } from '@prisma/client';
import { import { DocumentStatus, EnvelopeType, SendStatus, SigningStatus } from '@prisma/client';
DocumentStatus,
EnvelopeType,
RecipientRole,
SendStatus,
SigningStatus,
} from '@prisma/client';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { z } from 'zod'; import { z } from 'zod';
@ -162,9 +156,8 @@ export const canEnvelopeItemsBeModified = (
if ( if (
recipients.some( recipients.some(
(recipient) => (recipient) =>
recipient.role !== RecipientRole.CC && recipient.signingStatus === SigningStatus.SIGNED ||
(recipient.signingStatus === SigningStatus.SIGNED || recipient.sendStatus === SendStatus.SENT,
recipient.sendStatus === SendStatus.SENT),
) )
) { ) {
return false; return false;

View File

@ -1,37 +0,0 @@
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
export const isValidReturnTo = (returnTo?: string) => {
if (!returnTo) {
return false;
}
try {
// Decode if it's URL encoded
const decodedReturnTo = decodeURIComponent(returnTo);
const returnToUrl = new URL(decodedReturnTo, NEXT_PUBLIC_WEBAPP_URL());
if (returnToUrl.origin !== NEXT_PUBLIC_WEBAPP_URL()) {
return false;
}
return true;
} catch {
return false;
}
};
export const normalizeReturnTo = (returnTo?: string) => {
if (!returnTo) {
return undefined;
}
try {
// Decode if it's URL encoded
const decodedReturnTo = decodeURIComponent(returnTo);
const returnToUrl = new URL(decodedReturnTo, NEXT_PUBLIC_WEBAPP_URL());
return `${returnToUrl.pathname}${returnToUrl.search}${returnToUrl.hash}`;
} catch {
return undefined;
}
};

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