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
207 changed files with 5879 additions and 10940 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

@ -176,7 +176,7 @@ export const EnvelopeDownloadDialog = ({
{!isDownloadingState[generateDownloadKey(item.id, 'original')] && ( {!isDownloadingState[generateDownloadKey(item.id, 'original')] && (
<DownloadIcon className="mr-2 h-4 w-4" /> <DownloadIcon className="mr-2 h-4 w-4" />
)} )}
<Trans context="Original document (adjective)">Original</Trans> <Trans>Original</Trans>
</Button> </Button>
{envelopeStatus === DocumentStatus.COMPLETED && ( {envelopeStatus === DocumentStatus.COMPLETED && (
@ -190,7 +190,7 @@ export const EnvelopeDownloadDialog = ({
{!isDownloadingState[generateDownloadKey(item.id, 'signed')] && ( {!isDownloadingState[generateDownloadKey(item.id, 'signed')] && (
<DownloadIcon className="mr-2 h-4 w-4" /> <DownloadIcon className="mr-2 h-4 w-4" />
)} )}
<Trans context="Signed document (adjective)">Signed</Trans> <Trans>Signed</Trans>
</Button> </Button>
)} )}
</div> </div>

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

@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Plural, Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog'; import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { startRegistration } from '@simplewebauthn/browser'; import { startRegistration } from '@simplewebauthn/browser';
import { KeyRoundIcon } from 'lucide-react'; import { KeyRoundIcon } from 'lucide-react';
@ -209,11 +209,7 @@ export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCre
)) ))
.with('TOO_MANY_PASSKEYS', () => ( .with('TOO_MANY_PASSKEYS', () => (
<AlertDescription> <AlertDescription>
<Plural <Trans>You cannot have more than {MAXIMUM_PASSKEYS} passkeys.</Trans>
value={MAXIMUM_PASSKEYS}
one="You cannot have more than # passkey."
other="You cannot have more than # passkeys."
/>
</AlertDescription> </AlertDescription>
)) ))
.with('InvalidStateError', () => ( .with('InvalidStateError', () => (

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';
export type SignFieldNumberDialogProps = {
fieldMeta: TNumberFieldMeta;
};
export const SignFieldNumberDialog = createCallable<SignFieldNumberDialogProps, string | null>(
({ call, fieldMeta }) => {
const { t } = useLingui();
// Needs to be inside dialog for translation purposes.
const createNumberFieldSchema = (fieldMeta: TNumberFieldMeta) => { const createNumberFieldSchema = (fieldMeta: TNumberFieldMeta) => {
let schema = z.coerce.number({
invalid_type_error: msg`Please enter a valid number`.id,
});
const { numberFormat, minValue, maxValue } = fieldMeta; const { numberFormat, minValue, maxValue } = fieldMeta;
if (typeof minValue === 'number') {
schema = schema.min(minValue);
}
if (typeof maxValue === 'number') {
schema = schema.max(maxValue);
}
if (numberFormat) { if (numberFormat) {
const foundRegex = numberFormatValues.find((item) => item.value === numberFormat)?.regex; const foundRegex = numberFormatValues.find((item) => item.value === numberFormat)?.regex;
if (foundRegex) { if (!foundRegex) {
return z.string().refine( return schema;
}
return schema.refine(
(value) => { (value) => {
return foundRegex.test(value.toString()); return foundRegex.test(value.toString());
}, },
{ {
message: t`Number needs to be formatted as ${numberFormat}`, message: msg`Number needs to be formatted as ${numberFormat}`.id,
}, },
); );
} }
}
// Not gong to work with min/max numbers + number format return schema;
// 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;
}
});
}; };
export type SignFieldNumberDialogProps = {
fieldMeta: TNumberFieldMeta;
};
export const SignFieldNumberDialog = createCallable<SignFieldNumberDialogProps, number | null>(
({ call, fieldMeta }) => {
const { t } = useLingui();
const ZSignFieldNumberFormSchema = z.object({ const ZSignFieldNumberFormSchema = z.object({
number: createNumberFieldSchema(fieldMeta), number: createNumberFieldSchema(fieldMeta),
}); });

View File

@ -96,7 +96,7 @@ export const TemplateCreateDialog = ({ folderId }: TemplateCreateDialogProps) =>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="cursor-pointer" disabled={!user.emailVerified}> <Button className="cursor-pointer" disabled={!user.emailVerified}>
<FilePlus className="-ml-1 mr-2 h-4 w-4" /> <FilePlus className="-ml-1 mr-2 h-4 w-4" />
<Trans>Template (Legacy)</Trans> <Trans>New Template</Trans>
</Button> </Button>
</DialogTrigger> </DialogTrigger>

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

@ -1,49 +0,0 @@
import { useTransition } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import type { DateRange } from '@documenso/lib/types/search-params';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
type DateRangeFilterProps = {
currentRange: DateRange;
};
export const DateRangeFilter = ({ currentRange }: DateRangeFilterProps) => {
const { _ } = useLingui();
const [isPending, startTransition] = useTransition();
const updateSearchParams = useUpdateSearchParams();
const handleRangeChange = (value: string) => {
startTransition(() => {
updateSearchParams({
dateRange: value as DateRange,
page: 1,
});
});
};
return (
<div className="flex items-center gap-2">
<Select value={currentRange} onValueChange={handleRangeChange} disabled={isPending}>
<SelectTrigger className="w-48">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="last30days">{_(msg`Last 30 Days`)}</SelectItem>
<SelectItem value="last90days">{_(msg`Last 90 Days`)}</SelectItem>
<SelectItem value="lastYear">{_(msg`Last Year`)}</SelectItem>
<SelectItem value="allTime">{_(msg`All Time`)}</SelectItem>
</SelectContent>
</Select>
</div>
);
};

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">
<div className="flex w-full flex-row gap-x-4">
<EditorGenericFontSizeField className="w-full" formControl={form.control} /> <EditorGenericFontSizeField className="w-full" formControl={form.control} />
<div className="flex w-full flex-row gap-x-4">
<EditorGenericTextAlignField className="w-full" formControl={form.control} /> <EditorGenericTextAlignField className="w-full" formControl={form.control} />
<EditorGenericVerticalAlignField 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,27 +22,23 @@ 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);
@ -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">
<div className="flex w-full flex-row gap-x-4">
<EditorGenericFontSizeField className="w-full" formControl={form.control} /> <EditorGenericFontSizeField className="w-full" formControl={form.control} />
<div className="flex w-full flex-row gap-x-4">
<EditorGenericTextAlignField className="w-full" formControl={form.control} /> <EditorGenericTextAlignField className="w-full" formControl={form.control} />
<EditorGenericVerticalAlignField 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,8 +383,6 @@ 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 && ( {hasSocialAuthEnabled && (
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase"> <div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
<div className="bg-border h-px flex-1" /> <div className="bg-border h-px flex-1" />
@ -421,11 +416,7 @@ export const SignInForm = ({
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"
alt="Microsoft Logo"
src={'/static/microsoft.svg'}
/>
Microsoft Microsoft
</Button> </Button>
)} )}
@ -443,8 +434,6 @@ export const SignInForm = ({
{oidcProviderLabel || 'OIDC'} {oidcProviderLabel || 'OIDC'}
</Button> </Button>
)} )}
</>
)}
<Button <Button
type="button" type="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>To mark this document as viewed, you need to be logged in.</Trans>
) : (
<Trans> <Trans>
To mark this document as viewed, you need to be logged in as{' '} To mark this document as viewed, you need to be logged in as{' '}
<strong>{recipient.email}</strong> <strong>{recipient.email}</strong>
</Trans> </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

@ -1,15 +1,10 @@
import { type ReactNode, useState } from 'react'; import { type ReactNode, useState } from 'react';
import { useLingui } from '@lingui/react/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { EnvelopeType } from '@prisma/client';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { import { ErrorCode, type FileRejection, useDropzone } from 'react-dropzone';
ErrorCode as DropzoneErrorCode,
ErrorCode,
type FileRejection,
useDropzone,
} from 'react-dropzone';
import { Link, useNavigate, useParams } from 'react-router'; import { Link, useNavigate, useParams } from 'react-router';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
@ -21,26 +16,21 @@ import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/l
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones'; import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions'; import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TCreateEnvelopePayload } from '@documenso/trpc/server/envelope-router/create-envelope.types'; import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCurrentTeam } from '~/providers/team'; import { useCurrentTeam } from '~/providers/team';
export interface EnvelopeDropZoneWrapperProps { export interface DocumentDropZoneWrapperProps {
children: ReactNode; children: ReactNode;
type: EnvelopeType;
className?: string; className?: string;
} }
export const EnvelopeDropZoneWrapper = ({ export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZoneWrapperProps) => {
children, const { _ } = useLingui();
type,
className,
}: EnvelopeDropZoneWrapperProps) => {
const { t } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { user } = useSession(); const { user } = useSession();
const { folderId } = useParams(); const { folderId } = useParams();
@ -57,13 +47,13 @@ export const EnvelopeDropZoneWrapper = ({
TIME_ZONES.find((timezone) => timezone === Intl.DateTimeFormat().resolvedOptions().timeZone) ?? TIME_ZONES.find((timezone) => timezone === Intl.DateTimeFormat().resolvedOptions().timeZone) ??
DEFAULT_DOCUMENT_TIME_ZONE; DEFAULT_DOCUMENT_TIME_ZONE;
const { quota, remaining, refreshLimits, maximumEnvelopeItemCount } = useLimits(); const { quota, remaining, refreshLimits } = useLimits();
const { mutateAsync: createEnvelope } = trpc.envelope.create.useMutation(); const { mutateAsync: createDocument } = trpc.document.create.useMutation();
const isUploadDisabled = remaining.documents === 0 || !user.emailVerified; const isUploadDisabled = remaining.documents === 0 || !user.emailVerified;
const onFileDrop = async (files: File[]) => { const onFileDrop = async (file: File) => {
if (isUploadDisabled && IS_BILLING_ENABLED()) { if (isUploadDisabled && IS_BILLING_ENABLED()) {
await navigate(`/o/${organisation.url}/settings/billing`); await navigate(`/o/${organisation.url}/settings/billing`);
return; return;
@ -73,67 +63,51 @@ export const EnvelopeDropZoneWrapper = ({
setIsLoading(true); setIsLoading(true);
const payload = { const payload = {
folderId, title: file.name,
type,
title: files[0].name,
meta: {
timezone: userTimezone, timezone: userTimezone,
}, folderId: folderId ?? undefined,
} satisfies TCreateEnvelopePayload; } satisfies TCreateDocumentPayloadSchema;
const formData = new FormData(); const formData = new FormData();
formData.append('payload', JSON.stringify(payload)); formData.append('payload', JSON.stringify(payload));
formData.append('file', file);
for (const file of files) { const { envelopeId: id } = await createDocument(formData);
formData.append('files', file);
}
const { id } = await createEnvelope(formData);
void refreshLimits(); void refreshLimits();
toast({ toast({
title: type === EnvelopeType.DOCUMENT ? t`Document uploaded` : t`Template uploaded`, title: _(msg`Document uploaded`),
description: description: _(msg`Your document has been uploaded successfully.`),
type === EnvelopeType.DOCUMENT
? t`Your document has been uploaded successfully.`
: t`Your template has been uploaded successfully.`,
duration: 5000, duration: 5000,
}); });
if (type === EnvelopeType.DOCUMENT) {
analytics.capture('App: Document Uploaded', { analytics.capture('App: Document Uploaded', {
userId: user.id, userId: user.id,
documentId: id, documentId: id,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
}
const pathPrefix = await navigate(`${formatDocumentsPath(team.url)}/${id}/edit`);
type === EnvelopeType.DOCUMENT
? formatDocumentsPath(team.url)
: formatTemplatesPath(team.url);
await navigate(`${pathPrefix}/${id}/edit`);
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);
const errorMessage = match(error.code) const errorMessage = match(error.code)
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs`) .with('INVALID_DOCUMENT_FILE', () => msg`You cannot upload encrypted PDFs`)
.with( .with(
AppErrorCode.LIMIT_EXCEEDED, AppErrorCode.LIMIT_EXCEEDED,
() => t`You have reached your document limit for this month. Please upgrade your plan.`, () => msg`You have reached your document limit for this month. Please upgrade your plan.`,
) )
.with( .with(
'ENVELOPE_ITEM_LIMIT_EXCEEDED', 'ENVELOPE_ITEM_LIMIT_EXCEEDED',
() => t`You have reached the limit of the number of files per envelope`, () => msg`You have reached the limit of the number of files per envelope`,
) )
.otherwise(() => t`An error occurred during upload.`); .otherwise(() => msg`An error occurred while uploading your document.`);
toast({ toast({
title: t`Error`, title: _(msg`Error`),
description: errorMessage, description: _(errorMessage),
variant: 'destructive', variant: 'destructive',
duration: 7500, duration: 7500,
}); });
@ -147,20 +121,6 @@ export const EnvelopeDropZoneWrapper = ({
return; return;
} }
const maxItemsReached = fileRejections.some((fileRejection) =>
fileRejection.errors.some((error) => error.code === DropzoneErrorCode.TooManyFiles),
);
if (maxItemsReached) {
toast({
title: t`You cannot upload more than ${maximumEnvelopeItemCount} items per envelope.`,
duration: 5000,
variant: 'destructive',
});
return;
}
// Since users can only upload only one file (no multi-upload), we only handle the first file rejection // Since users can only upload only one file (no multi-upload), we only handle the first file rejection
const { file, errors } = fileRejections[0]; const { file, errors } = fileRejections[0];
@ -188,14 +148,14 @@ export const EnvelopeDropZoneWrapper = ({
const description = ( const description = (
<> <>
<span className="font-medium"> <span className="font-medium">
<Trans>{file.name} couldn't be uploaded:</Trans> {file.name} <Trans>couldn't be uploaded:</Trans>
</span> </span>
{errorNodes} {errorNodes}
</> </>
); );
toast({ toast({
title: t`Upload failed`, title: _(msg`Upload failed`),
description, description,
duration: 5000, duration: 5000,
variant: 'destructive', variant: 'destructive',
@ -205,11 +165,17 @@ export const EnvelopeDropZoneWrapper = ({
accept: { accept: {
'application/pdf': ['.pdf'], 'application/pdf': ['.pdf'],
}, },
multiple: true, //disabled: isUploadDisabled,
multiple: false,
maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT), maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT),
maxFiles: maximumEnvelopeItemCount, onDrop: ([acceptedFile]) => {
onDrop: (files) => void onFileDrop(files), if (acceptedFile) {
onDropRejected: onFileDropRejected, void onFileDrop(acceptedFile);
}
},
onDropRejected: (fileRejections) => {
onFileDropRejected(fileRejections);
},
noClick: true, noClick: true,
noDragEventsBubbling: true, noDragEventsBubbling: true,
}); });
@ -223,11 +189,7 @@ export const EnvelopeDropZoneWrapper = ({
<div className="bg-muted/60 fixed left-0 top-0 z-[9999] h-full w-full backdrop-blur-[4px]"> <div className="bg-muted/60 fixed left-0 top-0 z-[9999] h-full w-full backdrop-blur-[4px]">
<div className="pointer-events-none flex h-full w-full flex-col items-center justify-center"> <div className="pointer-events-none flex h-full w-full flex-col items-center justify-center">
<h2 className="text-foreground text-2xl font-semibold"> <h2 className="text-foreground text-2xl font-semibold">
{type === EnvelopeType.DOCUMENT ? (
<Trans>Upload Document</Trans> <Trans>Upload Document</Trans>
) : (
<Trans>Upload Template</Trans>
)}
</h2> </h2>
<p className="text-muted-foreground text-md mt-4"> <p className="text-muted-foreground text-md mt-4">
@ -262,7 +224,7 @@ export const EnvelopeDropZoneWrapper = ({
<div className="pointer-events-none flex h-1/2 w-full flex-col items-center justify-center"> <div className="pointer-events-none flex h-1/2 w-full flex-col items-center justify-center">
<Loader className="text-primary h-12 w-12 animate-spin" /> <Loader className="text-primary h-12 w-12 animate-spin" />
<p className="text-foreground mt-8 font-medium"> <p className="text-foreground mt-8 font-medium">
<Trans>Uploading</Trans> <Trans>Uploading document...</Trans>
</p> </p>
</div> </div>
</div> </div>

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

@ -3,7 +3,6 @@ import { useMemo, useState } from 'react';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { EnvelopeType } from '@prisma/client';
import { useNavigate, useParams } from 'react-router'; import { useNavigate, useParams } from 'react-router';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
@ -18,7 +17,7 @@ import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types'; import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { DocumentUploadButton as DocumentUploadButtonPrimitive } from '@documenso/ui/primitives/document-upload-button'; import { DocumentDropzone } from '@documenso/ui/primitives/document-upload';
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@ -29,11 +28,11 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCurrentTeam } from '~/providers/team'; import { useCurrentTeam } from '~/providers/team';
export type DocumentUploadButtonLegacyProps = { export type DocumentUploadButtonProps = {
className?: string; className?: string;
}; };
export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLegacyProps) => { export const DocumentUploadButton = ({ className }: DocumentUploadButtonProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { user } = useSession(); const { user } = useSession();
@ -76,10 +75,8 @@ export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLe
const payload = { const payload = {
title: file.name, title: file.name,
folderId: folderId ?? undefined,
meta: {
timezone: userTimezone, timezone: userTimezone,
}, folderId: folderId ?? undefined,
} satisfies TCreateDocumentPayloadSchema; } satisfies TCreateDocumentPayloadSchema;
const formData = new FormData(); const formData = new FormData();
@ -147,14 +144,12 @@ export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLe
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div> <div>
<DocumentUploadButtonPrimitive <DocumentDropzone
loading={isLoading} loading={isLoading}
disabled={remaining.documents === 0 || !user.emailVerified} disabled={remaining.documents === 0 || !user.emailVerified}
disabledMessage={disabledMessage} disabledMessage={disabledMessage}
onDrop={async (files) => onFileDrop(files[0])} onDrop={async (files) => onFileDrop(files[0])}
onDropRejected={onFileDropRejected} onDropRejected={onFileDropRejected}
type={EnvelopeType.DOCUMENT}
internalVersion="1"
/> />
</div> </div>
</TooltipTrigger> </TooltipTrigger>

View File

@ -18,7 +18,7 @@ import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/t
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TCreateEnvelopePayload } from '@documenso/trpc/server/envelope-router/create-envelope.types'; import type { TCreateEnvelopePayload } from '@documenso/trpc/server/envelope-router/create-envelope.types';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { DocumentUploadButton } from '@documenso/ui/primitives/document-upload-button'; import { DocumentDropzone } from '@documenso/ui/primitives/document-upload';
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@ -175,14 +175,13 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div> <div>
<DocumentUploadButton <DocumentDropzone
loading={isLoading} loading={isLoading}
disabled={remaining.documents === 0 || !user.emailVerified} disabled={remaining.documents === 0 || !user.emailVerified}
disabledMessage={disabledMessage} disabledMessage={disabledMessage}
onDrop={onFileDrop} onDrop={onFileDrop}
onDropRejected={onFileDropRejected} onDropRejected={onFileDropRejected}
type={type} type="envelope"
internalVersion="2"
maxFiles={maximumEnvelopeItemCount} maxFiles={maximumEnvelopeItemCount}
/> />
</div> </div>

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

@ -75,12 +75,14 @@ export const EnvelopeSignerCompleteDialog = () => {
accessAuthOptions?: TRecipientAccessAuth, accessAuthOptions?: TRecipientAccessAuth,
) => { ) => {
try { try {
await completeDocument({ const payload = {
token: recipient.token, token: recipient.token,
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId), documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
accessAuthOptions, authOptions: accessAuthOptions,
...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}), ...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}),
}); };
await completeDocument(payload);
analytics.capture('App: Recipient has completed signing', { analytics.capture('App: Recipient has completed signing', {
signerId: recipient.id, signerId: recipient.id,
@ -188,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

@ -6,6 +6,7 @@ import { FolderIcon, HomeIcon } from 'lucide-react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { IS_ENVELOPES_ENABLED } from '@documenso/lib/constants/app';
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { type TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema'; import { type TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
@ -16,11 +17,11 @@ import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog'; import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
import { FolderUpdateDialog } from '~/components/dialogs/folder-update-dialog'; import { FolderUpdateDialog } from '~/components/dialogs/folder-update-dialog';
import { TemplateCreateDialog } from '~/components/dialogs/template-create-dialog'; import { TemplateCreateDialog } from '~/components/dialogs/template-create-dialog';
import { DocumentUploadButtonLegacy } from '~/components/general/document/document-upload-button-legacy'; import { DocumentUploadButton } from '~/components/general/document/document-upload-button';
import { FolderCard, FolderCardEmpty } from '~/components/general/folder/folder-card'; import { FolderCard, FolderCardEmpty } from '~/components/general/folder/folder-card';
import { useCurrentTeam } from '~/providers/team'; import { useCurrentTeam } from '~/providers/team';
import { EnvelopeUploadButton } from '../envelope/envelope-upload-button'; import { EnvelopeUploadButton } from '../document/envelope-upload-button';
export type FolderGridProps = { export type FolderGridProps = {
type: FolderType; type: FolderType;
@ -98,12 +99,14 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
</div> </div>
<div className="flex gap-4 sm:flex-row sm:justify-end"> <div className="flex gap-4 sm:flex-row sm:justify-end">
{(IS_ENVELOPES_ENABLED || organisation.organisationClaim.flags.allowEnvelopes) && (
<EnvelopeUploadButton type={type} folderId={parentId || undefined} /> <EnvelopeUploadButton type={type} folderId={parentId || undefined} />
)}
{type === FolderType.DOCUMENT ? ( {type === FolderType.DOCUMENT ? (
<DocumentUploadButtonLegacy /> // If you delete this, delete the component as well. <DocumentUploadButton />
) : ( ) : (
<TemplateCreateDialog folderId={parentId ?? undefined} /> // If you delete this, delete the component as well. <TemplateCreateDialog folderId={parentId ?? undefined} />
)} )}
<FolderCreateDialog type={type} /> <FolderCreateDialog type={type} />

View File

@ -0,0 +1,171 @@
import { type ReactNode, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { Loader } from 'lucide-react';
import { ErrorCode, type FileRejection, useDropzone } from 'react-dropzone';
import { useNavigate, useParams } from 'react-router';
import { match } from 'ts-pattern';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import type { TCreateTemplatePayloadSchema } from '@documenso/trpc/server/template-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCurrentTeam } from '~/providers/team';
export interface TemplateDropZoneWrapperProps {
children: ReactNode;
className?: string;
}
export const TemplateDropZoneWrapper = ({ children, className }: TemplateDropZoneWrapperProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { folderId } = useParams();
const team = useCurrentTeam();
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
const onFileDrop = async (file: File) => {
try {
setIsLoading(true);
const payload = {
title: file.name,
folderId: folderId ?? undefined,
} satisfies TCreateTemplatePayloadSchema;
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append('file', file);
const { envelopeId: id } = await createTemplate(formData);
toast({
title: _(msg`Template uploaded`),
description: _(
msg`Your template has been uploaded successfully. You will be redirected to the template page.`,
),
duration: 5000,
});
await navigate(`${formatTemplatesPath(team.url)}/${id}/edit`);
} catch {
toast({
title: _(msg`Something went wrong`),
description: _(msg`Please try again later.`),
variant: 'destructive',
});
} finally {
setIsLoading(false);
}
};
const onFileDropRejected = (fileRejections: FileRejection[]) => {
if (!fileRejections.length) {
return;
}
// Since users can only upload only one file (no multi-upload), we only handle the first file rejection
const { file, errors } = fileRejections[0];
if (!errors.length) {
return;
}
const errorNodes = errors.map((error, index) => (
<span key={index} className="block">
{match(error.code)
.with(ErrorCode.FileTooLarge, () => (
<Trans>File is larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB</Trans>
))
.with(ErrorCode.FileInvalidType, () => <Trans>Only PDF files are allowed</Trans>)
.with(ErrorCode.FileTooSmall, () => <Trans>File is too small</Trans>)
.with(ErrorCode.TooManyFiles, () => (
<Trans>Only one file can be uploaded at a time</Trans>
))
.otherwise(() => (
<Trans>Unknown error</Trans>
))}
</span>
));
const description = (
<>
<span className="font-medium">
{file.name} <Trans>couldn't be uploaded:</Trans>
</span>
{errorNodes}
</>
);
toast({
title: _(msg`Upload failed`),
description,
duration: 5000,
variant: 'destructive',
});
};
const { getRootProps, getInputProps, isDragActive } = useDropzone({
accept: {
'application/pdf': ['.pdf'],
},
//disabled: isUploadDisabled,
multiple: false,
maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT),
onDrop: ([acceptedFile]) => {
if (acceptedFile) {
void onFileDrop(acceptedFile);
}
},
onDropRejected: (fileRejections) => {
onFileDropRejected(fileRejections);
},
noClick: true,
noDragEventsBubbling: true,
});
return (
<div {...getRootProps()} className={cn('relative min-h-screen', className)}>
<input {...getInputProps()} />
{children}
{isDragActive && (
<div className="bg-muted/60 fixed left-0 top-0 z-[9999] h-full w-full backdrop-blur-[4px]">
<div className="pointer-events-none flex h-full w-full flex-col items-center justify-center">
<h2 className="text-foreground text-2xl font-semibold">
<Trans>Upload Template</Trans>
</h2>
<p className="text-muted-foreground text-md mt-4">
<Trans>Drag and drop your PDF file here</Trans>
</p>
</div>
</div>
)}
{isLoading && (
<div className="bg-muted/30 absolute inset-0 z-50 backdrop-blur-[2px]">
<div className="pointer-events-none flex h-1/2 w-full flex-col items-center justify-center">
<Loader className="text-primary h-12 w-12 animate-spin" />
<p className="text-foreground mt-8 font-medium">
<Trans>Uploading template...</Trans>
</p>
</div>
</div>
)}
</div>
);
};

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

@ -2,49 +2,40 @@ import { useEffect, useMemo, useState, useTransition } from 'react';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { ChevronDownIcon, ChevronUpIcon, ChevronsUpDown, Loader } from 'lucide-react'; import { ChevronDownIcon, ChevronUpIcon, ChevronsUpDown, Loader } from 'lucide-react';
import { Link } from 'react-router';
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value'; import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import type { DateRange } from '@documenso/lib/types/search-params';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table'; import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table'; import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
export type OrganisationOverview = { export type SigningVolume = {
id: string; id: number;
name: string; name: string;
signingVolume: number; signingVolume: number;
createdAt: Date; createdAt: Date;
customerId: string; planId: string;
subscriptionStatus?: string;
isActive?: boolean;
teamCount?: number;
memberCount?: number;
}; };
type OrganisationOverviewTableProps = { type LeaderboardTableProps = {
organisations: OrganisationOverview[]; signingVolume: SigningVolume[];
totalPages: number; totalPages: number;
perPage: number; perPage: number;
page: number; page: number;
sortBy: 'name' | 'createdAt' | 'signingVolume'; sortBy: 'name' | 'createdAt' | 'signingVolume';
sortOrder: 'asc' | 'desc'; sortOrder: 'asc' | 'desc';
dateRange: DateRange;
}; };
export const AdminOrganisationOverviewTable = ({ export const AdminLeaderboardTable = ({
organisations, signingVolume,
totalPages, totalPages,
perPage, perPage,
page, page,
sortBy, sortBy,
sortOrder, sortOrder,
dateRange, }: LeaderboardTableProps) => {
}: OrganisationOverviewTableProps) => {
const { _, i18n } = useLingui(); const { _, i18n } = useLingui();
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
@ -76,16 +67,17 @@ export const AdminOrganisationOverviewTable = ({
cell: ({ row }) => { cell: ({ row }) => {
return ( return (
<div> <div>
<Link <a
className="hover:underline" className="text-primary underline"
to={`/admin/organisation-insights/${row.original.id}?dateRange=${dateRange}`} href={`https://dashboard.stripe.com/subscriptions/${row.original.planId}`}
target="_blank"
> >
{row.getValue('name')} {row.getValue('name')}
</Link> </a>
</div> </div>
); );
}, },
size: 240, size: 250,
}, },
{ {
header: () => ( header: () => (
@ -93,9 +85,7 @@ export const AdminOrganisationOverviewTable = ({
className="flex cursor-pointer items-center" className="flex cursor-pointer items-center"
onClick={() => handleColumnSort('signingVolume')} onClick={() => handleColumnSort('signingVolume')}
> >
<span className="whitespace-nowrap"> {_(msg`Signing Volume`)}
<Trans>Document Volume</Trans>
</span>
{sortBy === 'signingVolume' ? ( {sortBy === 'signingVolume' ? (
sortOrder === 'asc' ? ( sortOrder === 'asc' ? (
<ChevronUpIcon className="ml-2 h-4 w-4" /> <ChevronUpIcon className="ml-2 h-4 w-4" />
@ -109,23 +99,6 @@ export const AdminOrganisationOverviewTable = ({
), ),
accessorKey: 'signingVolume', accessorKey: 'signingVolume',
cell: ({ row }) => <div>{Number(row.getValue('signingVolume'))}</div>, cell: ({ row }) => <div>{Number(row.getValue('signingVolume'))}</div>,
size: 160,
},
{
header: () => {
return <Trans>Teams</Trans>;
},
accessorKey: 'teamCount',
cell: ({ row }) => <div>{Number(row.original.teamCount) || 0}</div>,
size: 120,
},
{
header: () => {
return <Trans>Members</Trans>;
},
accessorKey: 'memberCount',
cell: ({ row }) => <div>{Number(row.original.memberCount) || 0}</div>,
size: 160,
}, },
{ {
header: () => { header: () => {
@ -134,9 +107,7 @@ export const AdminOrganisationOverviewTable = ({
className="flex cursor-pointer items-center" className="flex cursor-pointer items-center"
onClick={() => handleColumnSort('createdAt')} onClick={() => handleColumnSort('createdAt')}
> >
<span className="whitespace-nowrap"> {_(msg`Created`)}
<Trans>Created</Trans>
</span>
{sortBy === 'createdAt' ? ( {sortBy === 'createdAt' ? (
sortOrder === 'asc' ? ( sortOrder === 'asc' ? (
<ChevronUpIcon className="ml-2 h-4 w-4" /> <ChevronUpIcon className="ml-2 h-4 w-4" />
@ -150,11 +121,10 @@ export const AdminOrganisationOverviewTable = ({
); );
}, },
accessorKey: 'createdAt', accessorKey: 'createdAt',
cell: ({ row }) => i18n.date(new Date(row.original.createdAt)), cell: ({ row }) => i18n.date(row.original.createdAt),
size: 120,
}, },
] satisfies DataTableColumnDef<OrganisationOverview>[]; ] satisfies DataTableColumnDef<SigningVolume>[];
}, [sortOrder, sortBy, dateRange]); }, [sortOrder, sortBy]);
useEffect(() => { useEffect(() => {
startTransition(() => { startTransition(() => {
@ -199,13 +169,13 @@ export const AdminOrganisationOverviewTable = ({
<Input <Input
className="my-6 flex flex-row gap-4" className="my-6 flex flex-row gap-4"
type="text" type="text"
placeholder={_(msg`Search by organisation name`)} placeholder={_(msg`Search by name or email`)}
value={searchString} value={searchString}
onChange={handleChange} onChange={handleChange}
/> />
<DataTable <DataTable
columns={columns} columns={columns}
data={organisations} data={signingVolume}
perPage={perPage} perPage={perPage}
currentPage={page} currentPage={page}
totalPages={totalPages} totalPages={totalPages}

View File

@ -93,31 +93,13 @@ export const AdminOrganisationsTable = ({
), ),
}, },
{ {
id: 'role', header: t`Status`,
header: t`Role`,
cell: ({ row }) => ( cell: ({ row }) => (
<Badge variant="neutral"> <Badge variant="neutral">
{row.original.owner.id === memberUserId ? t`Owner` : t`Member`} {row.original.owner.id === memberUserId ? t`Owner` : t`Member`}
</Badge> </Badge>
), ),
}, },
{
id: 'billingStatus',
header: t`Status`,
cell: ({ row }) => {
const subscription = row.original.subscription;
const isPaid = subscription && subscription.status === 'ACTIVE';
return (
<div
className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
isPaid ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
}`}
>
{isPaid ? t`Paid` : t`Free`}
</div>
);
},
},
{ {
header: t`Subscription`, header: t`Subscription`,
cell: ({ row }) => cell: ({ row }) =>
@ -186,7 +168,7 @@ export const AdminOrganisationsTable = ({
onPaginationChange={onPaginationChange} onPaginationChange={onPaginationChange}
columnVisibility={{ columnVisibility={{
owner: showOwnerColumn, owner: showOwnerColumn,
role: memberUserId !== undefined, status: memberUserId !== undefined,
}} }}
error={{ error={{
enable: isLoadingError, enable: isLoadingError,

View File

@ -1,287 +0,0 @@
import { useTransition } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Building2, Loader, TrendingUp, Users } from 'lucide-react';
import { Link } from 'react-router';
import { useNavigation } from 'react-router';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import type { OrganisationDetailedInsights } from '@documenso/lib/server-only/admin/get-organisation-detailed-insights';
import type { DateRange } from '@documenso/lib/types/search-params';
import type { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { Button } from '@documenso/ui/primitives/button';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { DateRangeFilter } from '~/components/filters/date-range-filter';
import { DocumentStatus } from '~/components/general/document/document-status';
type OrganisationInsightsTableProps = {
insights: OrganisationDetailedInsights;
page: number;
perPage: number;
dateRange: DateRange;
view: 'teams' | 'users' | 'documents';
};
export const OrganisationInsightsTable = ({
insights,
page,
perPage,
dateRange,
view,
}: OrganisationInsightsTableProps) => {
const { _, i18n } = useLingui();
const [isPending, startTransition] = useTransition();
const navigation = useNavigation();
const updateSearchParams = useUpdateSearchParams();
const isLoading = isPending || navigation.state === 'loading';
const onPaginationChange = (newPage: number, newPerPage: number) => {
startTransition(() => {
updateSearchParams({
page: newPage,
perPage: newPerPage,
});
});
};
const handleViewChange = (newView: 'teams' | 'users' | 'documents') => {
startTransition(() => {
updateSearchParams({
view: newView,
page: 1,
});
});
};
const teamsColumns = [
{
header: _(msg`Team Name`),
accessorKey: 'name',
cell: ({ row }) => <span className="block max-w-full truncate">{row.getValue('name')}</span>,
size: 240,
},
{
header: _(msg`Members`),
accessorKey: 'memberCount',
cell: ({ row }) => Number(row.getValue('memberCount')),
size: 120,
},
{
header: _(msg`Documents`),
accessorKey: 'documentCount',
cell: ({ row }) => Number(row.getValue('documentCount')),
size: 140,
},
{
header: _(msg`Created`),
accessorKey: 'createdAt',
cell: ({ row }) => i18n.date(new Date(row.getValue('createdAt'))),
size: 160,
},
] satisfies DataTableColumnDef<(typeof insights.teams)[number]>[];
const usersColumns = [
{
header: () => <span className="whitespace-nowrap">{_(msg`Name`)}</span>,
accessorKey: 'name',
cell: ({ row }) => (
<Link
className="block max-w-full truncate hover:underline"
to={`/admin/users/${row.original.id}`}
>
{(row.getValue('name') as string) || (row.getValue('email') as string)}
</Link>
),
size: 220,
},
{
header: () => <span className="whitespace-nowrap">{_(msg`Email`)}</span>,
accessorKey: 'email',
cell: ({ row }) => <span className="block max-w-full truncate">{row.getValue('email')}</span>,
size: 260,
},
{
header: () => <span className="whitespace-nowrap">{_(msg`Documents Created`)}</span>,
accessorKey: 'documentCount',
cell: ({ row }) => Number(row.getValue('documentCount')),
size: 180,
},
{
header: () => <span className="whitespace-nowrap">{_(msg`Documents Completed`)}</span>,
accessorKey: 'signedDocumentCount',
cell: ({ row }) => Number(row.getValue('signedDocumentCount')),
size: 180,
},
{
header: () => <span className="whitespace-nowrap">{_(msg`Joined`)}</span>,
accessorKey: 'createdAt',
cell: ({ row }) => i18n.date(new Date(row.getValue('createdAt'))),
size: 160,
},
] satisfies DataTableColumnDef<(typeof insights.users)[number]>[];
const documentsColumns = [
{
header: () => <span className="whitespace-nowrap">{_(msg`Title`)}</span>,
accessorKey: 'title',
cell: ({ row }) => (
<Link
className="block max-w-[200px] truncate hover:underline"
to={`/admin/documents/${row.original.id}`}
title={row.getValue('title') as string}
>
{row.getValue('title')}
</Link>
),
size: 200,
},
{
header: () => <span className="whitespace-nowrap">{_(msg`Status`)}</span>,
accessorKey: 'status',
cell: ({ row }) => (
<DocumentStatus status={row.getValue('status') as ExtendedDocumentStatus} />
),
size: 120,
},
{
header: () => <span className="whitespace-nowrap">{_(msg`Team`)}</span>,
accessorKey: 'teamName',
cell: ({ row }) => (
<span className="block max-w-[150px] truncate" title={row.getValue('teamName') as string}>
{row.getValue('teamName')}
</span>
),
size: 150,
},
{
header: () => <span className="whitespace-nowrap">{_(msg`Created`)}</span>,
accessorKey: 'createdAt',
cell: ({ row }) => i18n.date(new Date(row.getValue('createdAt'))),
size: 140,
},
{
header: () => <span className="whitespace-nowrap">{_(msg`Completed`)}</span>,
accessorKey: 'completedAt',
cell: ({ row }) => {
const completedAt = row.getValue('completedAt') as Date | null;
return completedAt ? i18n.date(new Date(completedAt)) : '-';
},
size: 140,
},
] satisfies DataTableColumnDef<(typeof insights.documents)[number]>[];
const getCurrentData = (): unknown[] => {
switch (view) {
case 'teams':
return insights.teams;
case 'users':
return insights.users;
case 'documents':
return insights.documents;
default:
return [];
}
};
const getCurrentColumns = (): DataTableColumnDef<unknown>[] => {
switch (view) {
case 'teams':
return teamsColumns as unknown as DataTableColumnDef<unknown>[];
case 'users':
return usersColumns as unknown as DataTableColumnDef<unknown>[];
case 'documents':
return documentsColumns as unknown as DataTableColumnDef<unknown>[];
default:
return [];
}
};
return (
<div className="relative">
{insights.summary && (
<div className="mb-6 grid grid-cols-2 gap-4 md:grid-cols-3">
<SummaryCard icon={Building2} title={_(msg`Teams`)} value={insights.summary.totalTeams} />
<SummaryCard icon={Users} title={_(msg`Members`)} value={insights.summary.totalMembers} />
<SummaryCard
icon={TrendingUp}
title={_(msg`Documents Completed`)}
value={insights.summary.volumeThisPeriod}
/>
</div>
)}
<div className="mb-6 flex items-center justify-between">
<div className="flex gap-2">
<Button
variant={view === 'teams' ? 'default' : 'outline'}
onClick={() => handleViewChange('teams')}
disabled={isLoading}
>
{_(msg`Teams`)}
</Button>
<Button
variant={view === 'users' ? 'default' : 'outline'}
onClick={() => handleViewChange('users')}
disabled={isLoading}
>
{_(msg`Users`)}
</Button>
<Button
variant={view === 'documents' ? 'default' : 'outline'}
onClick={() => handleViewChange('documents')}
disabled={isLoading}
>
{_(msg`Documents`)}
</Button>
</div>
<DateRangeFilter currentRange={dateRange} />
</div>
<div className={view === 'documents' ? 'overflow-hidden' : undefined}>
<DataTable<unknown, unknown>
columns={getCurrentColumns()}
data={getCurrentData()}
perPage={perPage}
currentPage={page}
totalPages={insights.totalPages}
onPaginationChange={onPaginationChange}
>
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
</div>
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
<Loader className="h-8 w-8 animate-spin text-gray-500" />
</div>
)}
</div>
);
};
const SummaryCard = ({
icon: Icon,
title,
value,
subtitle,
}: {
icon: React.ComponentType<{ className?: string }>;
title: string;
value: number;
subtitle?: string;
}) => (
<div className="bg-card flex items-start gap-x-2 rounded-lg border px-4 py-3">
<Icon className="text-muted-foreground h-4 w-4 items-start" />
<div className="-mt-0.5 space-y-2">
<p className="text-muted-foreground text-sm font-medium">{title}</p>
<p className="text-2xl font-bold">{value}</p>
{subtitle && <p className="text-muted-foreground text-xs">{subtitle}</p>}
</div>
</div>
);

View File

@ -56,14 +56,7 @@ export const UserOrganisationsTable = () => {
avatarFallback={row.original.name.slice(0, 1).toUpperCase()} avatarFallback={row.original.name.slice(0, 1).toUpperCase()}
primaryText={ primaryText={
<span className="text-foreground/80 font-semibold"> <span className="text-foreground/80 font-semibold">
{isPersonalLayoutMode {isPersonalLayoutMode ? _(msg`Personal`) : row.original.name}
? _(
msg({
message: `Personal`,
context: `Personal organisation (adjective)`,
}),
)
: row.original.name}
</span> </span>
} }
secondaryText={ secondaryText={

View File

@ -88,12 +88,14 @@ export default function Layout({ loaderData, params, matches }: Route.ComponentP
? { ? {
heading: msg`Organisation not found`, heading: msg`Organisation not found`,
subHeading: msg`404 Organisation not found`, subHeading: msg`404 Organisation not found`,
message: msg`The organisation you are looking for may have been removed, renamed or may have never existed.`, message: msg`The organisation you are looking for may have been removed, renamed or may have never
existed.`,
} }
: { : {
heading: msg`Team not found`, heading: msg`Team not found`,
subHeading: msg`404 Team not found`, subHeading: msg`404 Team not found`,
message: msg`The team you are looking for may have been removed, renamed or may have never existed.`, message: msg`The team you are looking for may have been removed, renamed or may have never
existed.`,
}, },
}} }}
primaryButton={ primaryButton={

View File

@ -114,13 +114,13 @@ export default function AdminLayout() {
variant="ghost" variant="ghost"
className={cn( className={cn(
'justify-start md:w-full', 'justify-start md:w-full',
pathname?.startsWith('/admin/organisation-insights') && 'bg-secondary', pathname?.startsWith('/admin/leaderboard') && 'bg-secondary',
)} )}
asChild asChild
> >
<Link to="/admin/organisation-insights"> <Link to="/admin/leaderboard">
<Trophy className="mr-2 h-5 w-5" /> <Trophy className="mr-2 h-5 w-5" />
<Trans>Organisation Insights</Trans> <Trans>Leaderboard</Trans>
</Link> </Link>
</Button> </Button>
@ -128,7 +128,7 @@ export default function AdminLayout() {
variant="ghost" variant="ghost"
className={cn( className={cn(
'justify-start md:w-full', 'justify-start md:w-full',
pathname?.startsWith('/admin/site-settings') && 'bg-secondary', pathname?.startsWith('/admin/banner') && 'bg-secondary',
)} )}
asChild asChild
> >

View File

@ -0,0 +1,77 @@
import { Trans } from '@lingui/react/macro';
import { getSigningVolume } from '@documenso/lib/server-only/admin/get-signing-volume';
import {
AdminLeaderboardTable,
type SigningVolume,
} from '~/components/tables/admin-leaderboard-table';
import type { Route } from './+types/leaderboard';
export async function loader({ request }: Route.LoaderArgs) {
const url = new URL(request.url);
const rawSortBy = url.searchParams.get('sortBy') || 'signingVolume';
const rawSortOrder = url.searchParams.get('sortOrder') || 'desc';
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const sortOrder = (['asc', 'desc'].includes(rawSortOrder) ? rawSortOrder : 'desc') as
| 'asc'
| 'desc';
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const sortBy = (
['name', 'createdAt', 'signingVolume'].includes(rawSortBy) ? rawSortBy : 'signingVolume'
) as 'name' | 'createdAt' | 'signingVolume';
const page = Number(url.searchParams.get('page')) || 1;
const perPage = Number(url.searchParams.get('perPage')) || 10;
const search = url.searchParams.get('search') || '';
const { leaderboard, totalPages } = await getSigningVolume({
search,
page,
perPage,
sortBy,
sortOrder,
});
const typedSigningVolume: SigningVolume[] = leaderboard.map((item) => ({
...item,
name: item.name || '',
createdAt: item.createdAt || new Date(),
}));
return {
signingVolume: typedSigningVolume,
totalPages,
page,
perPage,
sortBy,
sortOrder,
};
}
export default function Leaderboard({ loaderData }: Route.ComponentProps) {
const { signingVolume, totalPages, page, perPage, sortBy, sortOrder } = loaderData;
return (
<div>
<div className="flex items-center">
<h2 className="text-4xl font-semibold">
<Trans>Signing Volume</Trans>
</h2>
</div>
<div className="mt-8">
<AdminLeaderboardTable
signingVolume={signingVolume}
totalPages={totalPages}
page={page}
perPage={perPage}
sortBy={sortBy}
sortOrder={sortOrder}
/>
</div>
</div>
);
}

View File

@ -1,59 +0,0 @@
import { getOrganisationDetailedInsights } from '@documenso/lib/server-only/admin/get-organisation-detailed-insights';
import type { DateRange } from '@documenso/lib/types/search-params';
import { getAdminOrganisation } from '@documenso/trpc/server/admin-router/get-admin-organisation';
import { OrganisationInsightsTable } from '~/components/tables/organisation-insights-table';
import type { Route } from './+types/organisation-insights.$id';
export async function loader({ params, request }: Route.LoaderArgs) {
const { id } = params;
const url = new URL(request.url);
const page = Number(url.searchParams.get('page')) || 1;
const perPage = Number(url.searchParams.get('perPage')) || 10;
const dateRange = (url.searchParams.get('dateRange') || 'last30days') as DateRange;
const view = (url.searchParams.get('view') || 'teams') as 'teams' | 'users' | 'documents';
const [insights, organisation] = await Promise.all([
getOrganisationDetailedInsights({
organisationId: id,
page,
perPage,
dateRange,
view,
}),
getAdminOrganisation({ organisationId: id }),
]);
return {
organisationId: id,
organisationName: organisation.name,
insights,
page,
perPage,
dateRange,
view,
};
}
export default function OrganisationInsights({ loaderData }: Route.ComponentProps) {
const { insights, page, perPage, dateRange, view, organisationName } = loaderData;
return (
<div>
<div className="flex items-center justify-between">
<h2 className="text-4xl font-semibold">{organisationName}</h2>
</div>
<div className="mt-8">
<OrganisationInsightsTable
insights={insights}
page={page}
perPage={perPage}
dateRange={dateRange}
view={view}
/>
</div>
</div>
);
}

View File

@ -1,91 +0,0 @@
import { Trans } from '@lingui/react/macro';
import { getOrganisationInsights } from '@documenso/lib/server-only/admin/get-signing-volume';
import type { DateRange } from '@documenso/lib/types/search-params';
import { DateRangeFilter } from '~/components/filters/date-range-filter';
import {
AdminOrganisationOverviewTable,
type OrganisationOverview,
} from '~/components/tables/admin-organisation-overview-table';
import type { Route } from './+types/organisation-insights._index';
export async function loader({ request }: Route.LoaderArgs) {
const url = new URL(request.url);
const rawSortBy = url.searchParams.get('sortBy') || 'signingVolume';
const rawSortOrder = url.searchParams.get('sortOrder') || 'desc';
const isSortOrder = (value: string): value is 'asc' | 'desc' =>
value === 'asc' || value === 'desc';
const isSortBy = (value: string): value is 'name' | 'createdAt' | 'signingVolume' =>
value === 'name' || value === 'createdAt' || value === 'signingVolume';
const sortOrder: 'asc' | 'desc' = isSortOrder(rawSortOrder) ? rawSortOrder : 'desc';
const sortBy: 'name' | 'createdAt' | 'signingVolume' = isSortBy(rawSortBy)
? rawSortBy
: 'signingVolume';
const page = Number(url.searchParams.get('page')) || 1;
const perPage = Number(url.searchParams.get('perPage')) || 10;
const search = url.searchParams.get('search') || '';
const dateRange = (url.searchParams.get('dateRange') || 'last30days') as DateRange;
const { organisations, totalPages } = await getOrganisationInsights({
search,
page,
perPage,
sortBy,
sortOrder,
dateRange,
});
const typedOrganisations: OrganisationOverview[] = organisations.map((item) => ({
id: String(item.id),
name: item.name || '',
signingVolume: item.signingVolume,
createdAt: item.createdAt || new Date(),
customerId: item.customerId || '',
subscriptionStatus: item.subscriptionStatus,
teamCount: item.teamCount || 0,
memberCount: item.memberCount || 0,
}));
return {
organisations: typedOrganisations,
totalPages,
page,
perPage,
sortBy,
sortOrder,
dateRange,
};
}
export default function Organisations({ loaderData }: Route.ComponentProps) {
const { organisations, totalPages, page, perPage, sortBy, sortOrder, dateRange } = loaderData;
return (
<div>
<div className="flex items-center justify-between">
<h2 className="text-4xl font-semibold">
<Trans>Organisation Insights</Trans>
</h2>
<DateRangeFilter currentRange={dateRange} />
</div>
<div className="mt-8">
<AdminOrganisationOverviewTable
organisations={organisations}
totalPages={totalPages}
page={page}
perPage={perPage}
sortBy={sortBy}
sortOrder={sortOrder}
dateRange={dateRange}
/>
</div>
</div>
);
}

View File

@ -142,7 +142,8 @@ export default function OrganisationGroupSettingsPage({ params }: Route.Componen
404: { 404: {
heading: msg`Organisation not found`, heading: msg`Organisation not found`,
subHeading: msg`404 Organisation not found`, subHeading: msg`404 Organisation not found`,
message: msg`The organisation you are looking for may have been removed, renamed or may have never existed.`, message: msg`The organisation you are looking for may have been removed, renamed or may have never
existed.`,
}, },
}} }}
primaryButton={ primaryButton={

View File

@ -59,7 +59,8 @@ export default function UserPage({ params }: { params: { id: number } }) {
404: { 404: {
heading: msg`User not found`, heading: msg`User not found`,
subHeading: msg`404 User not found`, subHeading: msg`404 User not found`,
message: msg`The user you are looking for may have been removed, renamed or may have never existed.`, message: msg`The user you are looking for may have been removed, renamed or may have never
existed.`,
}, },
}} }}
primaryButton={ primaryButton={

View File

@ -117,7 +117,8 @@ export default function OrganisationEmailDomainSettingsPage({ params }: Route.Co
404: { 404: {
heading: msg`Email domain not found`, heading: msg`Email domain not found`,
subHeading: msg`404 Email domain not found`, subHeading: msg`404 Email domain not found`,
message: msg`The email domain you are looking for may have been removed, renamed or may have never existed.`, message: msg`The email domain you are looking for may have been removed, renamed or may have never
existed.`,
}, },
}} }}
primaryButton={ primaryButton={

View File

@ -89,7 +89,8 @@ export default function OrganisationGroupSettingsPage({ params }: Route.Componen
404: { 404: {
heading: msg`Organisation group not found`, heading: msg`Organisation group not found`,
subHeading: msg`404 Organisation group not found`, subHeading: msg`404 Organisation group not found`,
message: msg`The organisation group you are looking for may have been removed, renamed or may have never existed.`, message: msg`The organisation group you are looking for may have been removed, renamed or may have never
existed.`,
}, },
}} }}
primaryButton={ primaryButton={

View File

@ -60,7 +60,8 @@ export default function Layout() {
404: { 404: {
heading: msg`Team not found`, heading: msg`Team not found`,
subHeading: msg`404 Team not found`, subHeading: msg`404 Team not found`,
message: msg`The team you are looking for may have been removed, renamed or may have never existed.`, message: msg`The team you are looking for may have been removed, renamed or may have never
existed.`,
}, },
}} }}
primaryButton={ primaryButton={

View File

@ -71,7 +71,8 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
404: { 404: {
heading: msg`Not found`, heading: msg`Not found`,
subHeading: msg`404 Not found`, subHeading: msg`404 Not found`,
message: msg`The document you are looking for may have been removed, renamed or may have never existed.`, message: msg`The document you are looking for may have been removed, renamed or may have never
existed.`,
}, },
}} }}
primaryButton={ primaryButton={
@ -126,11 +127,7 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
position="bottom" position="bottom"
> >
<span> <span>
<Plural <Trans>{envelope.recipients.length} Recipient(s)</Trans>
value={envelope.recipients.length}
one="# Recipient"
other="# Recipients"
/>
</span> </span>
</StackAvatarsWithTooltip> </StackAvatarsWithTooltip>
</div> </div>

View File

@ -82,7 +82,8 @@ export default function EnvelopeEditorPage({ params }: Route.ComponentProps) {
404: { 404: {
heading: msg`Not found`, heading: msg`Not found`,
subHeading: msg`404 Not found`, subHeading: msg`404 Not found`,
message: msg`The document you are looking for may have been removed, renamed or may have never existed.`, message: msg`The document you are looking for may have been removed, renamed or may have never
existed.`,
}, },
}} }}
primaryButton={ primaryButton={

View File

@ -1,7 +1,6 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { EnvelopeType } from '@prisma/client';
import { FolderType, OrganisationType } from '@prisma/client'; import { FolderType, OrganisationType } from '@prisma/client';
import { useParams, useSearchParams } from 'react-router'; import { useParams, useSearchParams } from 'react-router';
import { Link } from 'react-router'; import { Link } from 'react-router';
@ -19,9 +18,9 @@ import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/av
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs'; import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { DocumentMoveToFolderDialog } from '~/components/dialogs/document-move-to-folder-dialog'; import { DocumentMoveToFolderDialog } from '~/components/dialogs/document-move-to-folder-dialog';
import { DocumentDropZoneWrapper } from '~/components/general/document/document-drop-zone-wrapper';
import { DocumentSearch } from '~/components/general/document/document-search'; import { DocumentSearch } from '~/components/general/document/document-search';
import { DocumentStatus } from '~/components/general/document/document-status'; import { DocumentStatus } from '~/components/general/document/document-status';
import { EnvelopeDropZoneWrapper } from '~/components/general/envelope/envelope-drop-zone-wrapper';
import { FolderGrid } from '~/components/general/folder/folder-grid'; import { FolderGrid } from '~/components/general/folder/folder-grid';
import { PeriodSelector } from '~/components/general/period-selector'; import { PeriodSelector } from '~/components/general/period-selector';
import { DocumentsTable } from '~/components/tables/documents-table'; import { DocumentsTable } from '~/components/tables/documents-table';
@ -109,8 +108,9 @@ export default function DocumentsPage() {
} }
}, [data?.stats]); }, [data?.stats]);
// Todo: Envelopes - Change the dropzone wrapper to create to V2 documents after we're ready.
return ( return (
<EnvelopeDropZoneWrapper type={EnvelopeType.DOCUMENT}> <DocumentDropZoneWrapper>
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8"> <div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
<FolderGrid type={FolderType.DOCUMENT} parentId={folderId ?? null} /> <FolderGrid type={FolderType.DOCUMENT} parentId={folderId ?? null} />
@ -210,6 +210,6 @@ export default function DocumentsPage() {
/> />
)} )}
</div> </div>
</EnvelopeDropZoneWrapper> </DocumentDropZoneWrapper>
); );
} }

View File

@ -109,7 +109,8 @@ export default function WebhookPage({ params }: Route.ComponentProps) {
404: { 404: {
heading: msg`Webhook not found`, heading: msg`Webhook not found`,
subHeading: msg`404 Webhook not found`, subHeading: msg`404 Webhook not found`,
message: msg`The webhook you are looking for may have been removed, renamed or may have never existed.`, message: msg`The webhook you are looking for may have been removed, renamed or may have never
existed.`,
}, },
}} }}
primaryButton={ primaryButton={

View File

@ -66,7 +66,8 @@ export default function TemplatePage({ params }: Route.ComponentProps) {
404: { 404: {
heading: msg`Not found`, heading: msg`Not found`,
subHeading: msg`404 Not found`, subHeading: msg`404 Not found`,
message: msg`The template you are looking for may have been removed, renamed or may have never existed.`, message: msg`The template you are looking for may have been removed, renamed or may have never
existed.`,
}, },
}} }}
primaryButton={ primaryButton={

View File

@ -1,5 +1,4 @@
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { EnvelopeType } from '@prisma/client';
import { Bird } from 'lucide-react'; import { Bird } from 'lucide-react';
import { useParams, useSearchParams } from 'react-router'; import { useParams, useSearchParams } from 'react-router';
@ -9,8 +8,8 @@ import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/t
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
import { EnvelopeDropZoneWrapper } from '~/components/general/envelope/envelope-drop-zone-wrapper';
import { FolderGrid } from '~/components/general/folder/folder-grid'; import { FolderGrid } from '~/components/general/folder/folder-grid';
import { TemplateDropZoneWrapper } from '~/components/general/template/template-drop-zone-wrapper';
import { TemplatesTable } from '~/components/tables/templates-table'; import { TemplatesTable } from '~/components/tables/templates-table';
import { useCurrentTeam } from '~/providers/team'; import { useCurrentTeam } from '~/providers/team';
import { appMetaTags } from '~/utils/meta'; import { appMetaTags } from '~/utils/meta';
@ -38,7 +37,7 @@ export default function TemplatesPage() {
}); });
return ( return (
<EnvelopeDropZoneWrapper type={EnvelopeType.TEMPLATE}> <TemplateDropZoneWrapper>
<div className="mx-auto max-w-screen-xl px-4 md:px-8"> <div className="mx-auto max-w-screen-xl px-4 md:px-8">
<FolderGrid type={FolderType.TEMPLATE} parentId={folderId ?? null} /> <FolderGrid type={FolderType.TEMPLATE} parentId={folderId ?? null} />
@ -86,6 +85,6 @@ export default function TemplatesPage() {
</div> </div>
</div> </div>
</div> </div>
</EnvelopeDropZoneWrapper> </TemplateDropZoneWrapper>
); );
} }

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.8" "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(
'/envelope/item/: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,

View File

@ -2,19 +2,12 @@ import { lingui } from '@lingui/vite-plugin';
import { reactRouter } from '@react-router/dev/vite'; import { reactRouter } from '@react-router/dev/vite';
import autoprefixer from 'autoprefixer'; import autoprefixer from 'autoprefixer';
import serverAdapter from 'hono-react-router-adapter/vite'; import serverAdapter from 'hono-react-router-adapter/vite';
import { createRequire } from 'node:module';
import path from 'node:path'; import path from 'node:path';
import tailwindcss from 'tailwindcss'; import tailwindcss from 'tailwindcss';
import { defineConfig, normalizePath } from 'vite'; import { defineConfig } from 'vite';
import macrosPlugin from 'vite-plugin-babel-macros'; import macrosPlugin from 'vite-plugin-babel-macros';
import { viteStaticCopy } from 'vite-plugin-static-copy';
import tsconfigPaths from 'vite-tsconfig-paths'; import tsconfigPaths from 'vite-tsconfig-paths';
const require = createRequire(import.meta.url);
const pdfjsDistPath = path.dirname(require.resolve('pdfjs-dist/package.json'));
const cMapsDir = normalizePath(path.join(pdfjsDistPath, 'cmaps'));
/** /**
* Note: We load the env variables externally so we can have runtime enviroment variables * Note: We load the env variables externally so we can have runtime enviroment variables
* for docker. * for docker.
@ -32,14 +25,6 @@ export default defineConfig({
strictPort: true, strictPort: true,
}, },
plugins: [ plugins: [
viteStaticCopy({
targets: [
{
src: cMapsDir,
dest: 'static',
},
],
}),
reactRouter(), reactRouter(),
macrosPlugin(), macrosPlugin(),
lingui(), lingui(),

Binary file not shown.

View File

@ -4,7 +4,6 @@
FROM node:22-alpine3.20 AS base FROM node:22-alpine3.20 AS base
RUN apk add --no-cache openssl RUN apk add --no-cache openssl
RUN apk add --no-cache font-freefont
########################### ###########################

111
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "@documenso/root", "name": "@documenso/root",
"version": "2.0.8", "version": "1.13.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@documenso/root", "name": "@documenso/root",
"version": "2.0.8", "version": "1.13.1",
"workspaces": [ "workspaces": [
"apps/*", "apps/*",
"packages/*" "packages/*"
@ -52,7 +52,6 @@
"trpc-to-openapi": "2.4.0", "trpc-to-openapi": "2.4.0",
"turbo": "^1.9.3", "turbo": "^1.9.3",
"vite": "^6.3.5", "vite": "^6.3.5",
"vite-plugin-static-copy": "^3.1.4",
"zod-openapi": "^4.2.4", "zod-openapi": "^4.2.4",
"zod-prisma-types": "3.3.5" "zod-prisma-types": "3.3.5"
}, },
@ -101,7 +100,7 @@
}, },
"apps/remix": { "apps/remix": {
"name": "@documenso/remix", "name": "@documenso/remix",
"version": "2.0.8", "version": "1.13.1",
"dependencies": { "dependencies": {
"@cantoo/pdf-lib": "^2.5.2", "@cantoo/pdf-lib": "^2.5.2",
"@documenso/api": "*", "@documenso/api": "*",
@ -130,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",
@ -177,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",
@ -12318,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",
@ -19113,13 +19103,10 @@
} }
}, },
"node_modules/fdir": { "node_modules/fdir": {
"version": "6.5.0", "version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"license": "MIT", "license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": { "peerDependencies": {
"picomatch": "^3 || ^4" "picomatch": "^3 || ^4"
}, },
@ -33483,13 +33470,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.15", "version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fdir": "^6.5.0", "fdir": "^6.4.4",
"picomatch": "^4.0.3" "picomatch": "^4.0.2"
}, },
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=12.0.0"
@ -33499,9 +33486,9 @@
} }
}, },
"node_modules/tinyglobby/node_modules/picomatch": { "node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.3", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@ -35948,76 +35935,6 @@
"vite": ">=2" "vite": ">=2"
} }
}, },
"node_modules/vite-plugin-static-copy": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.4.tgz",
"integrity": "sha512-iCmr4GSw4eSnaB+G8zc2f4dxSuDjbkjwpuBLLGvQYR9IW7rnDzftnUjOH5p4RYR+d4GsiBqXRvzuFhs5bnzVyw==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^3.6.0",
"p-map": "^7.0.3",
"picocolors": "^1.1.1",
"tinyglobby": "^0.2.15"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"peerDependencies": {
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
"node_modules/vite-plugin-static-copy/node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/vite-plugin-static-copy/node_modules/p-map": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz",
"integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/vite-plugin-static-copy/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/vite-tsconfig-paths": { "node_modules/vite-tsconfig-paths": {
"version": "5.1.4", "version": "5.1.4",
"resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz",

View File

@ -1,6 +1,6 @@
{ {
"private": true, "private": true,
"version": "2.0.8", "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",
@ -45,12 +45,6 @@
"@commitlint/config-conventional": "^17.7.0", "@commitlint/config-conventional": "^17.7.0",
"@lingui/cli": "^5.2.0", "@lingui/cli": "^5.2.0",
"@prisma/client": "^6.18.0", "@prisma/client": "^6.18.0",
"@trpc/client": "11.7.0",
"@trpc/react-query": "11.7.0",
"@trpc/server": "11.7.0",
"@ts-rest/core": "^3.52.1",
"@ts-rest/open-api": "^3.52.1",
"@ts-rest/serverless": "^3.52.1",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"eslint": "^8.40.0", "eslint": "^8.40.0",
@ -65,13 +59,18 @@
"prisma-json-types-generator": "^3.6.2", "prisma-json-types-generator": "^3.6.2",
"prisma-kysely": "^1.8.0", "prisma-kysely": "^1.8.0",
"rimraf": "^5.0.1", "rimraf": "^5.0.1",
"turbo": "^1.9.3",
"@trpc/client": "11.7.0",
"@trpc/react-query": "11.7.0",
"@trpc/server": "11.7.0",
"superjson": "^2.2.5", "superjson": "^2.2.5",
"trpc-to-openapi": "2.4.0", "trpc-to-openapi": "2.4.0",
"turbo": "^1.9.3",
"vite": "^6.3.5",
"vite-plugin-static-copy": "^3.1.4",
"zod-openapi": "^4.2.4", "zod-openapi": "^4.2.4",
"zod-prisma-types": "3.3.5" "@ts-rest/core": "^3.52.1",
"@ts-rest/open-api": "^3.52.1",
"@ts-rest/serverless": "^3.52.1",
"zod-prisma-types": "3.3.5",
"vite": "^6.3.5"
}, },
"name": "@documenso/root", "name": "@documenso/root",
"workspaces": [ "workspaces": [

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 = (
row: number,
column: number,
width: 'full' | 'column' = 'column',
) => {
const alignmentGridStartX = 31; const alignmentGridStartX = 31;
const alignmentGridStartY = 19; const alignmentGridStartY = 19.02;
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',

View File

@ -9,7 +9,6 @@ import { seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates'; import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { apiSignin } from '../fixtures/authentication'; import { apiSignin } from '../fixtures/authentication';
import { expectTextToBeVisible } from '../fixtures/generic';
test.describe.configure({ mode: 'parallel' }); test.describe.configure({ mode: 'parallel' });
@ -82,23 +81,20 @@ test('[TEAMS]: can create a document inside a document folder', async ({ page })
redirectPath: `/t/${team.url}/documents/f/${teamFolder.id}`, redirectPath: `/t/${team.url}/documents/f/${teamFolder.id}`,
}); });
// Upload document. const fileInput = page.locator('input[type="file"]').nth(1);
const [fileChooser] = await Promise.all([ await fileInput.waitFor({ state: 'attached' });
page.waitForEvent('filechooser'),
page.getByRole('button', { name: 'Document (Legacy)' }).click(),
]);
await fileChooser.setFiles( await fileInput.setInputFiles(
path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf'), path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf'),
); );
await page.waitForTimeout(3000); await page.waitForTimeout(3000);
await expectTextToBeVisible(page, 'documenso-supporter-pledge.pdf'); await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible();
await page.goto(`/t/${team.url}/documents/f/${teamFolder.id}`); await page.goto(`/t/${team.url}/documents/f/${teamFolder.id}`);
await expectTextToBeVisible(page, 'documenso-supporter-pledge.pdf'); await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible();
}); });
test('[TEAMS]: can pin a document folder', async ({ page }) => { test('[TEAMS]: can pin a document folder', async ({ page }) => {
@ -372,7 +368,7 @@ test('[TEAMS]: can create a template inside a template folder', async ({ page })
await expect(page.getByText('Team Client Templates')).toBeVisible(); await expect(page.getByText('Team Client Templates')).toBeVisible();
await page.getByRole('button', { name: 'Template (Legacy)' }).click(); await page.getByRole('button', { name: 'New Template' }).click();
await page.getByText('Upload Template Document').click(); await page.getByText('Upload Template Document').click();
@ -386,11 +382,11 @@ test('[TEAMS]: can create a template inside a template folder', async ({ page })
await page.waitForTimeout(3000); await page.waitForTimeout(3000);
// Expect redirect. // Expect redirect.
await expectTextToBeVisible(page, 'documenso-supporter-pledge.pdf'); await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible();
// Return to folder and verify file is visible. // Return to folder and verify file is visible.
await page.goto(`/t/${team.url}/templates/f/${folder.id}`); await page.goto(`/t/${team.url}/templates/f/${folder.id}`);
await expectTextToBeVisible(page, 'documenso-supporter-pledge.pdf'); await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible();
}); });
test('[TEAMS]: can pin a template folder', async ({ page }) => { test('[TEAMS]: can pin a template folder', async ({ page }) => {
@ -846,7 +842,7 @@ test('[TEAMS]: documents inherit folder visibility', async ({ page }) => {
// Upload document. // Upload document.
const [fileChooser] = await Promise.all([ const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser'), page.waitForEvent('filechooser'),
page.getByRole('button', { name: 'Document (Legacy)' }).click(), page.getByRole('button', { name: 'Upload Document' }).click(),
]); ]);
await fileChooser.setFiles( await fileChooser.setFiles(
@ -855,7 +851,7 @@ test('[TEAMS]: documents inherit folder visibility', async ({ page }) => {
await page.waitForTimeout(3000); await page.waitForTimeout(3000);
await expectTextToBeVisible(page, 'documenso-supporter-pledge.pdf'); await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible();
await expect(page.getByRole('combobox').filter({ hasText: 'Admins only' })).toBeVisible(); await expect(page.getByRole('combobox').filter({ hasText: 'Admins only' })).toBeVisible();
}); });

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

@ -1,4 +1,4 @@
import { Plural, Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { Heading, Img, Section, Text } from '../components'; import { Heading, Img, Section, Text } from '../components';
@ -46,11 +46,7 @@ export const TemplateAccessAuth2FA = ({
</Section> </Section>
<Text className="mt-4 text-center text-sm text-slate-600"> <Text className="mt-4 text-center text-sm text-slate-600">
<Plural <Trans>This code will expire in {expiresInMinutes} minutes.</Trans>
value={expiresInMinutes}
one="This code will expire in # minute."
other="This code will expire in # minutes."
/>
</Text> </Text>
<Text className="mt-4 text-center text-sm text-slate-500"> <Text className="mt-4 text-center text-sm text-slate-500">

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,

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