mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
Compare commits
12 Commits
feat/chang
...
feat/envel
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a0d2d1a82 | |||
| a26a740fe5 | |||
| f48813bb3c | |||
| 304c519c30 | |||
| 0eef4cd7e6 | |||
| bddaa5ec66 | |||
| 3be0d84786 | |||
| 50572435ad | |||
| 6f70548bb5 | |||
| 0da8e7dbc6 | |||
| bf89bc781b | |||
| eec2307634 |
@ -127,12 +127,12 @@ export const EnvelopeDistributeDialog = ({ envelope, trigger }: EnvelopeDistribu
|
|||||||
|
|
||||||
const distributionMethod = watch('meta.distributionMethod');
|
const distributionMethod = watch('meta.distributionMethod');
|
||||||
|
|
||||||
const everySignerHasSignature = useMemo(
|
const recipientsMissingSignatureFields = useMemo(
|
||||||
() =>
|
() =>
|
||||||
envelope.recipients
|
envelope.recipients.filter(
|
||||||
.filter((recipient) => recipient.role === RecipientRole.SIGNER)
|
(recipient) =>
|
||||||
.every((recipient) =>
|
recipient.role === RecipientRole.SIGNER &&
|
||||||
envelope.fields.some(
|
!envelope.fields.some(
|
||||||
(field) => field.type === FieldType.SIGNATURE && field.recipientId === recipient.id,
|
(field) => field.type === FieldType.SIGNATURE && field.recipientId === recipient.id,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -178,7 +178,7 @@ export const EnvelopeDistributeDialog = ({ envelope, trigger }: EnvelopeDistribu
|
|||||||
<Trans>Recipients will be able to sign the document once sent</Trans>
|
<Trans>Recipients will be able to sign the document once sent</Trans>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{everySignerHasSignature ? (
|
{recipientsMissingSignatureFields.length === 0 ? (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
<fieldset disabled={isSubmitting}>
|
<fieldset disabled={isSubmitting}>
|
||||||
@ -350,6 +350,8 @@ export const EnvelopeDistributeDialog = ({ envelope, trigger }: EnvelopeDistribu
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ul className="text-muted-foreground divide-y">
|
<ul className="text-muted-foreground divide-y">
|
||||||
|
{/* Todo: Envelopes - I don't think this section shows up */}
|
||||||
|
|
||||||
{recipients.length === 0 && (
|
{recipients.length === 0 && (
|
||||||
<li className="flex flex-col items-center justify-center py-6 text-sm">
|
<li className="flex flex-col items-center justify-center py-6 text-sm">
|
||||||
<Trans>No recipients</Trans>
|
<Trans>No recipients</Trans>
|
||||||
@ -427,10 +429,13 @@ export const EnvelopeDistributeDialog = ({ envelope, trigger }: EnvelopeDistribu
|
|||||||
<>
|
<>
|
||||||
<Alert variant="warning">
|
<Alert variant="warning">
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans>
|
<Trans>The following signers are missing signature fields:</Trans>
|
||||||
Some signers have not been assigned a signature field. Please assign at least 1
|
|
||||||
signature field to each signer before proceeding.
|
<ul className="ml-2 mt-1 list-inside list-disc">
|
||||||
</Trans>
|
{recipientsMissingSignatureFields.map((recipient) => (
|
||||||
|
<li key={recipient.id}>{recipient.email}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
|
|||||||
@ -1,40 +1,15 @@
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { msg } from '@lingui/core/macro';
|
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
|
||||||
import { createCallable } from 'react-call';
|
import { createCallable } from 'react-call';
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import type { TDropdownFieldMeta } from '@documenso/lib/types/field-meta';
|
import type { TDropdownFieldMeta } from '@documenso/lib/types/field-meta';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
CommandDialog,
|
||||||
DialogContent,
|
CommandEmpty,
|
||||||
DialogDescription,
|
CommandGroup,
|
||||||
DialogFooter,
|
CommandInput,
|
||||||
DialogHeader,
|
CommandItem,
|
||||||
DialogTitle,
|
CommandList,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/command';
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormMessage,
|
|
||||||
} from '@documenso/ui/primitives/form/form';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@documenso/ui/primitives/select';
|
|
||||||
|
|
||||||
const ZSignFieldDropdownFormSchema = z.object({
|
|
||||||
dropdown: z.string().min(1, { message: msg`Option is required`.id }),
|
|
||||||
});
|
|
||||||
|
|
||||||
type TSignFieldDropdownFormSchema = z.infer<typeof ZSignFieldDropdownFormSchema>;
|
|
||||||
|
|
||||||
export type SignFieldDropdownDialogProps = {
|
export type SignFieldDropdownDialogProps = {
|
||||||
fieldMeta: TDropdownFieldMeta;
|
fieldMeta: TDropdownFieldMeta;
|
||||||
@ -46,72 +21,20 @@ export const SignFieldDropdownDialog = createCallable<SignFieldDropdownDialogPro
|
|||||||
|
|
||||||
const values = fieldMeta.values?.map((value) => value.value) ?? [];
|
const values = fieldMeta.values?.map((value) => value.value) ?? [];
|
||||||
|
|
||||||
const form = useForm<TSignFieldDropdownFormSchema>({
|
|
||||||
resolver: zodResolver(ZSignFieldDropdownFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
dropdown: fieldMeta.defaultValue,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={true} onOpenChange={(value) => (!value ? call.end(null) : null)}>
|
<CommandDialog open={true} onOpenChange={(value) => (!value ? call.end(null) : null)}>
|
||||||
<DialogContent position="center">
|
<CommandInput placeholder={t`Select an option`} />
|
||||||
<DialogHeader>
|
<CommandList>
|
||||||
<DialogTitle>
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
<Trans>Sign Dropdown Field</Trans>
|
<CommandGroup heading={t`Options`}>
|
||||||
</DialogTitle>
|
|
||||||
|
|
||||||
<DialogDescription className="mt-4">
|
|
||||||
<Trans>Select a value to sign into the field</Trans>
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit((data) => call.end(data.dropdown))}>
|
|
||||||
<fieldset
|
|
||||||
className="flex h-full flex-col space-y-4"
|
|
||||||
disabled={form.formState.isSubmitting}
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="dropdown"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Select {...field} onValueChange={field.onChange}>
|
|
||||||
<SelectTrigger className="bg-background">
|
|
||||||
<SelectValue placeholder={t`Select an option`} />
|
|
||||||
</SelectTrigger>
|
|
||||||
|
|
||||||
<SelectContent>
|
|
||||||
{values.map((value, i) => (
|
{values.map((value, i) => (
|
||||||
<SelectItem key={i} value={value}>
|
<CommandItem onSelect={() => call.end(value)} key={i} value={value}>
|
||||||
{value}
|
{value}
|
||||||
</SelectItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</CommandGroup>
|
||||||
</Select>
|
</CommandList>
|
||||||
</FormControl>
|
</CommandDialog>
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button type="button" variant="secondary" onClick={() => call.end(null)}>
|
|
||||||
<Trans>Cancel</Trans>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button type="submit">
|
|
||||||
<Trans>Sign</Trans>
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -45,26 +45,32 @@ const ZDropdownFieldFormSchema = z
|
|||||||
.min(1, {
|
.min(1, {
|
||||||
message: msg`Dropdown must have at least one option`.id,
|
message: msg`Dropdown must have at least one option`.id,
|
||||||
})
|
})
|
||||||
.refine(
|
.superRefine((values, ctx) => {
|
||||||
(data) => {
|
const seen = new Map<string, number[]>(); // value → indices
|
||||||
// Todo: Envelopes - This doesn't work.
|
values.forEach((item, index) => {
|
||||||
console.log({
|
const key = item.value;
|
||||||
data,
|
if (!seen.has(key)) {
|
||||||
|
seen.set(key, []);
|
||||||
|
}
|
||||||
|
seen.get(key)!.push(index);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data) {
|
for (const [key, indices] of seen) {
|
||||||
const values = data.map((item) => item.value);
|
if (indices.length > 1 && key.trim() !== '') {
|
||||||
return new Set(values).size === values.length;
|
for (const i of indices) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: msg`Duplicate values are not allowed`.id,
|
||||||
|
path: [i, 'value'],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return true;
|
}
|
||||||
},
|
}
|
||||||
{
|
}),
|
||||||
message: 'Duplicate values are not allowed',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
required: z.boolean().optional(),
|
required: z.boolean().optional(),
|
||||||
readOnly: z.boolean().optional(),
|
readOnly: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
// Todo: Envelopes - This doesn't work
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
// Default value must be one of the available options
|
// Default value must be one of the available options
|
||||||
@ -111,7 +117,20 @@ export const EditorFieldDropdownForm = ({
|
|||||||
|
|
||||||
const addValue = () => {
|
const addValue = () => {
|
||||||
const currentValues = form.getValues('values') || [];
|
const currentValues = form.getValues('values') || [];
|
||||||
const newValues = [...currentValues, { value: 'New option' }];
|
|
||||||
|
let newValue = 'New option';
|
||||||
|
|
||||||
|
// Iterate to create a unique value
|
||||||
|
for (let i = 0; i < currentValues.length; i++) {
|
||||||
|
newValue = `New option ${i + 1}`;
|
||||||
|
if (currentValues.some((item) => item.value === `New option ${i + 1}`)) {
|
||||||
|
newValue = `New option ${i + 1}`;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newValues = [...currentValues, { value: newValue }];
|
||||||
|
|
||||||
form.setValue('values', newValues);
|
form.setValue('values', newValues);
|
||||||
};
|
};
|
||||||
@ -127,6 +146,10 @@ export const EditorFieldDropdownForm = ({
|
|||||||
newValues.splice(index, 1);
|
newValues.splice(index, 1);
|
||||||
|
|
||||||
form.setValue('values', newValues);
|
form.setValue('values', newValues);
|
||||||
|
|
||||||
|
if (form.getValues('defaultValue') === newValues[index].value) {
|
||||||
|
form.setValue('defaultValue', undefined);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -163,20 +186,26 @@ export const EditorFieldDropdownForm = ({
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Select
|
||||||
// Todo: Envelopes - This is buggy, removing/adding should update the default value.
|
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value}
|
value={field.value === null ? '-1' : field.value}
|
||||||
onValueChange={(val) => field.onChange(val)}
|
onValueChange={(value) => field.onChange(value === undefined ? null : value)}
|
||||||
>
|
>
|
||||||
|
{/* Todo: Envelopes - THis is cooked */}
|
||||||
<SelectTrigger className="text-muted-foreground bg-background w-full">
|
<SelectTrigger className="text-muted-foreground bg-background w-full">
|
||||||
<SelectValue placeholder={t`Default Value`} />
|
<SelectValue placeholder={t`Default Value`} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent position="popper">
|
<SelectContent position="popper">
|
||||||
{(formValues.values || []).map((item, index) => (
|
{(formValues.values || [])
|
||||||
|
.filter((item) => item.value)
|
||||||
|
.map((item, index) => (
|
||||||
<SelectItem key={index} value={item.value || ''}>
|
<SelectItem key={index} value={item.value || ''}>
|
||||||
{item.value}
|
{item.value}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<SelectItem value={'-1'}>
|
||||||
|
<Trans>None</Trans>
|
||||||
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@ -1,15 +1,32 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import { PlusIcon, Trash } from 'lucide-react';
|
import { PlusIcon, Trash } from 'lucide-react';
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import { type TRadioFieldMeta as RadioFieldMeta } from '@documenso/lib/types/field-meta';
|
import {
|
||||||
|
type TRadioFieldMeta as RadioFieldMeta,
|
||||||
|
ZRadioFieldMeta,
|
||||||
|
} from '@documenso/lib/types/field-meta';
|
||||||
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
||||||
import { Form, FormControl, FormField, FormItem } from '@documenso/ui/primitives/form/form';
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@documenso/ui/primitives/form/form';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@documenso/ui/primitives/select';
|
||||||
import { Separator } from '@documenso/ui/primitives/separator';
|
import { Separator } from '@documenso/ui/primitives/separator';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -17,19 +34,13 @@ import {
|
|||||||
EditorGenericRequiredField,
|
EditorGenericRequiredField,
|
||||||
} from './editor-field-generic-field-forms';
|
} from './editor-field-generic-field-forms';
|
||||||
|
|
||||||
const ZRadioFieldFormSchema = z
|
const ZRadioFieldFormSchema = ZRadioFieldMeta.pick({
|
||||||
.object({
|
label: true,
|
||||||
label: z.string().optional(),
|
direction: true,
|
||||||
values: z
|
values: true,
|
||||||
.object({ id: z.number(), checked: z.boolean(), value: z.string() })
|
required: true,
|
||||||
.array()
|
readOnly: true,
|
||||||
.min(1)
|
}).refine(
|
||||||
.optional(),
|
|
||||||
required: z.boolean().optional(),
|
|
||||||
readOnly: z.boolean().optional(),
|
|
||||||
direction: z.enum(['vertical', 'horizontal']).optional(),
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
(data) => {
|
||||||
// There cannot be more than one checked option
|
// There cannot be more than one checked option
|
||||||
if (data.values) {
|
if (data.values) {
|
||||||
@ -42,7 +53,7 @@ const ZRadioFieldFormSchema = z
|
|||||||
message: 'There cannot be more than one checked option',
|
message: 'There cannot be more than one checked option',
|
||||||
path: ['values'],
|
path: ['values'],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
type TRadioFieldFormSchema = z.infer<typeof ZRadioFieldFormSchema>;
|
type TRadioFieldFormSchema = z.infer<typeof ZRadioFieldFormSchema>;
|
||||||
|
|
||||||
@ -58,6 +69,8 @@ export const EditorFieldRadioForm = ({
|
|||||||
},
|
},
|
||||||
onValueChange,
|
onValueChange,
|
||||||
}: EditorFieldRadioFormProps) => {
|
}: EditorFieldRadioFormProps) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
const form = useForm<TRadioFieldFormSchema>({
|
const form = useForm<TRadioFieldFormSchema>({
|
||||||
resolver: zodResolver(ZRadioFieldFormSchema),
|
resolver: zodResolver(ZRadioFieldFormSchema),
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
@ -103,7 +116,6 @@ export const EditorFieldRadioForm = ({
|
|||||||
onValueChange({
|
onValueChange({
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
...validatedFormValues.data,
|
...validatedFormValues.data,
|
||||||
direction: validatedFormValues.data.direction || 'vertical',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [formValues]);
|
}, [formValues]);
|
||||||
@ -111,7 +123,35 @@ export const EditorFieldRadioForm = ({
|
|||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form>
|
<form>
|
||||||
<fieldset className="flex flex-col gap-2 pb-2">
|
<fieldset className="flex flex-col gap-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="direction"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
<Trans>Direction</Trans>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select value={field.value} onValueChange={field.onChange}>
|
||||||
|
<SelectTrigger className="text-muted-foreground bg-background w-full">
|
||||||
|
<SelectValue placeholder={t`Select direction`} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent position="popper">
|
||||||
|
<SelectItem value="vertical">
|
||||||
|
<Trans>Vertical</Trans>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="horizontal">
|
||||||
|
<Trans>Horizontal</Trans>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<EditorGenericRequiredField formControl={form.control} />
|
<EditorGenericRequiredField formControl={form.control} />
|
||||||
|
|
||||||
<EditorGenericReadOnlyField formControl={form.control} />
|
<EditorGenericReadOnlyField formControl={form.control} />
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import type {
|
|||||||
TRemovedSignedFieldWithTokenMutationSchema,
|
TRemovedSignedFieldWithTokenMutationSchema,
|
||||||
TSignFieldWithTokenMutationSchema,
|
TSignFieldWithTokenMutationSchema,
|
||||||
} from '@documenso/trpc/server/field-router/schema';
|
} from '@documenso/trpc/server/field-router/schema';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
|
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
@ -157,12 +156,7 @@ export const DocumentSigningRadioField = ({
|
|||||||
{!field.inserted && (
|
{!field.inserted && (
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
onValueChange={(value) => handleSelectItem(value)}
|
onValueChange={(value) => handleSelectItem(value)}
|
||||||
className={cn(
|
className="z-10 my-0.5 gap-y-1"
|
||||||
'z-10 my-0.5 gap-1',
|
|
||||||
parsedFieldMeta.direction === 'horizontal'
|
|
||||||
? 'flex flex-row flex-wrap'
|
|
||||||
: 'flex flex-col gap-y-1',
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{values?.map((item, index) => (
|
{values?.map((item, index) => (
|
||||||
<div key={index} className="flex items-center">
|
<div key={index} className="flex items-center">
|
||||||
@ -187,14 +181,7 @@ export const DocumentSigningRadioField = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{field.inserted && (
|
{field.inserted && (
|
||||||
<RadioGroup
|
<RadioGroup className="my-0.5 gap-y-1">
|
||||||
className={cn(
|
|
||||||
'my-0.5 gap-1',
|
|
||||||
parsedFieldMeta.direction === 'horizontal'
|
|
||||||
? 'flex flex-row flex-wrap'
|
|
||||||
: 'flex flex-col gap-y-1',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{values?.map((item, index) => (
|
{values?.map((item, index) => (
|
||||||
<div key={index} className="flex items-center">
|
<div key={index} className="flex items-center">
|
||||||
<RadioGroupItem
|
<RadioGroupItem
|
||||||
|
|||||||
@ -96,7 +96,7 @@ export const EnvelopeEditorFieldDragDrop = ({
|
|||||||
selectedRecipientId,
|
selectedRecipientId,
|
||||||
selectedEnvelopeItemId,
|
selectedEnvelopeItemId,
|
||||||
}: EnvelopeEditorFieldDragDropProps) => {
|
}: EnvelopeEditorFieldDragDropProps) => {
|
||||||
const { envelope, editorFields, isTemplate } = useCurrentEnvelopeEditor();
|
const { envelope, editorFields, isTemplate, getRecipientColorKey } = useCurrentEnvelopeEditor();
|
||||||
|
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
@ -262,6 +262,10 @@ export const EnvelopeEditorFieldDragDrop = ({
|
|||||||
};
|
};
|
||||||
}, [onMouseClick, onMouseMove, selectedField]);
|
}, [onMouseClick, onMouseMove, selectedField]);
|
||||||
|
|
||||||
|
const selectedRecipientColor = useMemo(() => {
|
||||||
|
return selectedRecipientId ? getRecipientColorKey(selectedRecipientId) : 'green';
|
||||||
|
}, [selectedRecipientId, getRecipientColorKey]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-2 gap-x-2 gap-y-2.5">
|
<div className="grid grid-cols-2 gap-x-2 gap-y-2.5">
|
||||||
@ -273,12 +277,23 @@ export const EnvelopeEditorFieldDragDrop = ({
|
|||||||
onClick={() => setSelectedField(field.type)}
|
onClick={() => setSelectedField(field.type)}
|
||||||
onMouseDown={() => setSelectedField(field.type)}
|
onMouseDown={() => setSelectedField(field.type)}
|
||||||
data-selected={selectedField === field.type ? true : undefined}
|
data-selected={selectedField === field.type ? true : undefined}
|
||||||
className="group flex h-12 cursor-pointer items-center justify-center rounded-lg border border-gray-200 px-4 transition-colors hover:border-blue-300 hover:bg-blue-50"
|
className={cn(
|
||||||
|
'group flex h-12 cursor-pointer items-center justify-center rounded-lg border border-gray-200 px-4 transition-colors',
|
||||||
|
RECIPIENT_COLOR_STYLES[selectedRecipientColor].fieldButton,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
||||||
field.className,
|
field.className,
|
||||||
|
{
|
||||||
|
'group-hover:text-recipient-green': selectedRecipientColor === 'green',
|
||||||
|
'group-hover:text-recipient-blue': selectedRecipientColor === 'blue',
|
||||||
|
'group-hover:text-recipient-purple': selectedRecipientColor === 'purple',
|
||||||
|
'group-hover:text-recipient-orange': selectedRecipientColor === 'orange',
|
||||||
|
'group-hover:text-recipient-yellow': selectedRecipientColor === 'yellow',
|
||||||
|
'group-hover:text-recipient-pink': selectedRecipientColor === 'pink',
|
||||||
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{field.type !== FieldType.SIGNATURE && <field.icon className="h-4 w-4" />}
|
{field.type !== FieldType.SIGNATURE && <field.icon className="h-4 w-4" />}
|
||||||
@ -292,8 +307,7 @@ export const EnvelopeEditorFieldDragDrop = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center rounded-[2px] bg-white ring-2 transition duration-200 [container-type:size]',
|
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center rounded-[2px] bg-white ring-2 transition duration-200 [container-type:size]',
|
||||||
// selectedSignerStyles?.base,
|
RECIPIENT_COLOR_STYLES[selectedRecipientColor].base,
|
||||||
RECIPIENT_COLOR_STYLES.yellow.base, // Todo: Envelopes
|
|
||||||
{
|
{
|
||||||
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
|
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
|
||||||
'dark:text-black/60': isFieldWithinBounds,
|
'dark:text-black/60': isFieldWithinBounds,
|
||||||
|
|||||||
@ -3,15 +3,12 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import type { FieldType } from '@prisma/client';
|
import type { FieldType } from '@prisma/client';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { Layer } from 'konva/lib/Layer';
|
|
||||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||||
import type { Transformer } from 'konva/lib/shapes/Transformer';
|
import type { Transformer } from 'konva/lib/shapes/Transformer';
|
||||||
import { CopyPlusIcon, SquareStackIcon, TrashIcon } from 'lucide-react';
|
import { CopyPlusIcon, SquareStackIcon, TrashIcon } from 'lucide-react';
|
||||||
import type { RenderParameters } from 'pdfjs-dist/types/src/display/api';
|
|
||||||
import { usePageContext } from 'react-pdf';
|
|
||||||
|
|
||||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
|
||||||
import type { TLocalField } from '@documenso/lib/client-only/hooks/use-editor-fields';
|
import type { TLocalField } from '@documenso/lib/client-only/hooks/use-editor-fields';
|
||||||
|
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
|
||||||
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
||||||
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||||
import { FIELD_META_DEFAULT_VALUES } from '@documenso/lib/types/field-meta';
|
import { FIELD_META_DEFAULT_VALUES } from '@documenso/lib/types/field-meta';
|
||||||
@ -26,27 +23,10 @@ import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
|
|||||||
import { fieldButtonList } from './envelope-editor-fields-drag-drop';
|
import { fieldButtonList } from './envelope-editor-fields-drag-drop';
|
||||||
|
|
||||||
export default function EnvelopeEditorFieldsPageRenderer() {
|
export default function EnvelopeEditorFieldsPageRenderer() {
|
||||||
const pageContext = usePageContext();
|
|
||||||
|
|
||||||
if (!pageContext) {
|
|
||||||
throw new Error('Unable to find Page context.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { _className, page, rotate, scale } = pageContext;
|
|
||||||
|
|
||||||
if (!page) {
|
|
||||||
throw new Error('Attempted to render page canvas, but no page was specified.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const { envelope, editorFields, getRecipientColorKey } = useCurrentEnvelopeEditor();
|
const { envelope, editorFields, getRecipientColorKey } = useCurrentEnvelopeEditor();
|
||||||
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
||||||
|
|
||||||
const canvasElement = useRef<HTMLCanvasElement>(null);
|
|
||||||
const konvaContainer = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const stage = useRef<Konva.Stage | null>(null);
|
|
||||||
const pageLayer = useRef<Layer | null>(null);
|
|
||||||
const interactiveTransformer = useRef<Transformer | null>(null);
|
const interactiveTransformer = useRef<Transformer | null>(null);
|
||||||
|
|
||||||
const [selectedKonvaFieldGroups, setSelectedKonvaFieldGroups] = useState<Konva.Group[]>([]);
|
const [selectedKonvaFieldGroups, setSelectedKonvaFieldGroups] = useState<Konva.Group[]>([]);
|
||||||
@ -54,10 +34,17 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
const [isFieldChanging, setIsFieldChanging] = useState(false);
|
const [isFieldChanging, setIsFieldChanging] = useState(false);
|
||||||
const [pendingFieldCreation, setPendingFieldCreation] = useState<Konva.Rect | null>(null);
|
const [pendingFieldCreation, setPendingFieldCreation] = useState<Konva.Rect | null>(null);
|
||||||
|
|
||||||
const viewport = useMemo(
|
const {
|
||||||
() => page.getViewport({ scale, rotation: rotate }),
|
stage,
|
||||||
[page, rotate, scale],
|
pageLayer,
|
||||||
);
|
canvasElement,
|
||||||
|
konvaContainer,
|
||||||
|
pageContext,
|
||||||
|
scaledViewport,
|
||||||
|
unscaledViewport,
|
||||||
|
} = usePageRenderer(({ stage, pageLayer }) => createPageCanvas(stage, pageLayer));
|
||||||
|
|
||||||
|
const { _className, scale } = pageContext;
|
||||||
|
|
||||||
const localPageFields = useMemo(
|
const localPageFields = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -68,44 +55,6 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
[editorFields.localFields, pageContext.pageNumber],
|
[editorFields.localFields, pageContext.pageNumber],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Custom renderer from Konva examples.
|
|
||||||
useEffect(
|
|
||||||
function drawPageOnCanvas() {
|
|
||||||
if (!page) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { current: canvas } = canvasElement;
|
|
||||||
const { current: container } = konvaContainer;
|
|
||||||
|
|
||||||
if (!canvas || !container) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderContext: RenderParameters = {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
canvasContext: canvas.getContext('2d', { alpha: false }) as CanvasRenderingContext2D,
|
|
||||||
viewport,
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancellable = page.render(renderContext);
|
|
||||||
const runningTask = cancellable;
|
|
||||||
|
|
||||||
cancellable.promise.catch(() => {
|
|
||||||
// Intentionally empty
|
|
||||||
});
|
|
||||||
|
|
||||||
void cancellable.promise.then(() => {
|
|
||||||
createPageCanvas(container);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
runningTask.cancel();
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[page, viewport],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleResizeOrMove = (event: KonvaEventObject<Event>) => {
|
const handleResizeOrMove = (event: KonvaEventObject<Event>) => {
|
||||||
console.log('Field resized or moved');
|
console.log('Field resized or moved');
|
||||||
|
|
||||||
@ -120,6 +69,7 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
const fieldGroup = event.target as Konva.Group;
|
const fieldGroup = event.target as Konva.Group;
|
||||||
const fieldFormId = fieldGroup.id();
|
const fieldFormId = fieldGroup.id();
|
||||||
|
|
||||||
|
// Note: This values are scaled.
|
||||||
const {
|
const {
|
||||||
width: fieldPixelWidth,
|
width: fieldPixelWidth,
|
||||||
height: fieldPixelHeight,
|
height: fieldPixelHeight,
|
||||||
@ -130,7 +80,8 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
skipShadow: true,
|
skipShadow: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { height: pageHeight, width: pageWidth } = getBoundingClientRect(container);
|
const pageHeight = scaledViewport.height;
|
||||||
|
const pageWidth = scaledViewport.width;
|
||||||
|
|
||||||
// Calculate x and y as a percentage of the page width and height
|
// Calculate x and y as a percentage of the page width and height
|
||||||
const positionPercentX = (fieldX / pageWidth) * 100;
|
const positionPercentX = (fieldX / pageWidth) * 100;
|
||||||
@ -165,7 +116,7 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderFieldOnLayer = (field: TLocalField) => {
|
const renderFieldOnLayer = (field: TLocalField) => {
|
||||||
if (!pageLayer.current || !interactiveTransformer.current) {
|
if (!pageLayer.current) {
|
||||||
console.error('Layer not loaded yet');
|
console.error('Layer not loaded yet');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -174,7 +125,8 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
const isFieldEditable =
|
const isFieldEditable =
|
||||||
recipient !== undefined && canRecipientFieldsBeModified(recipient, envelope.fields);
|
recipient !== undefined && canRecipientFieldsBeModified(recipient, envelope.fields);
|
||||||
|
|
||||||
const { fieldGroup, isFirstRender } = renderField({
|
const { fieldGroup } = renderField({
|
||||||
|
scale,
|
||||||
pageLayer: pageLayer.current,
|
pageLayer: pageLayer.current,
|
||||||
field: {
|
field: {
|
||||||
renderId: field.formId,
|
renderId: field.formId,
|
||||||
@ -183,8 +135,8 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
inserted: false,
|
inserted: false,
|
||||||
fieldMeta: field.fieldMeta,
|
fieldMeta: field.fieldMeta,
|
||||||
},
|
},
|
||||||
pageWidth: viewport.width,
|
pageWidth: unscaledViewport.width,
|
||||||
pageHeight: viewport.height,
|
pageHeight: unscaledViewport.height,
|
||||||
color: getRecipientColorKey(field.recipientId),
|
color: getRecipientColorKey(field.recipientId),
|
||||||
editable: isFieldEditable,
|
editable: isFieldEditable,
|
||||||
mode: 'edit',
|
mode: 'edit',
|
||||||
@ -210,24 +162,14 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the initial Konva page canvas and initialize all fields and interactions.
|
* Initialize the Konva page canvas and all fields and interactions.
|
||||||
*/
|
*/
|
||||||
const createPageCanvas = (container: HTMLDivElement) => {
|
const createPageCanvas = (currentStage: Konva.Stage, currentPageLayer: Konva.Layer) => {
|
||||||
stage.current = new Konva.Stage({
|
|
||||||
container,
|
|
||||||
width: viewport.width,
|
|
||||||
height: viewport.height,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create the main layer for interactive elements.
|
|
||||||
pageLayer.current = new Konva.Layer();
|
|
||||||
stage.current?.add(pageLayer.current);
|
|
||||||
|
|
||||||
// Initialize snap guides layer
|
// Initialize snap guides layer
|
||||||
// snapGuideLayer.current = initializeSnapGuides(stage.current);
|
// snapGuideLayer.current = initializeSnapGuides(stage.current);
|
||||||
|
|
||||||
// Add transformer for resizing and rotating.
|
// Add transformer for resizing and rotating.
|
||||||
interactiveTransformer.current = createInteractiveTransformer(stage.current, pageLayer.current);
|
interactiveTransformer.current = createInteractiveTransformer(currentStage, currentPageLayer);
|
||||||
|
|
||||||
// Render the fields.
|
// Render the fields.
|
||||||
for (const field of localPageFields) {
|
for (const field of localPageFields) {
|
||||||
@ -235,12 +177,12 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle stage click to deselect.
|
// Handle stage click to deselect.
|
||||||
stage.current?.on('click', (e) => {
|
currentStage.on('click', (e) => {
|
||||||
removePendingField();
|
removePendingField();
|
||||||
|
|
||||||
if (e.target === stage.current) {
|
if (e.target === stage.current) {
|
||||||
setSelectedFields([]);
|
setSelectedFields([]);
|
||||||
pageLayer.current?.batchDraw();
|
currentPageLayer.batchDraw();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -267,12 +209,12 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
setSelectedFields([e.target]);
|
setSelectedFields([e.target]);
|
||||||
};
|
};
|
||||||
|
|
||||||
stage.current?.on('dragstart', onDragStartOrEnd);
|
currentStage.on('dragstart', onDragStartOrEnd);
|
||||||
stage.current?.on('dragend', onDragStartOrEnd);
|
currentStage.on('dragend', onDragStartOrEnd);
|
||||||
stage.current?.on('transformstart', () => setIsFieldChanging(true));
|
currentStage.on('transformstart', () => setIsFieldChanging(true));
|
||||||
stage.current?.on('transformend', () => setIsFieldChanging(false));
|
currentStage.on('transformend', () => setIsFieldChanging(false));
|
||||||
|
|
||||||
pageLayer.current.batchDraw();
|
currentPageLayer.batchDraw();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -284,7 +226,10 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
* - Selecting multiple fields
|
* - Selecting multiple fields
|
||||||
* - Selecting empty area to create fields
|
* - Selecting empty area to create fields
|
||||||
*/
|
*/
|
||||||
const createInteractiveTransformer = (stage: Konva.Stage, layer: Konva.Layer) => {
|
const createInteractiveTransformer = (
|
||||||
|
currentStage: Konva.Stage,
|
||||||
|
currentPageLayer: Konva.Layer,
|
||||||
|
) => {
|
||||||
const transformer = new Konva.Transformer({
|
const transformer = new Konva.Transformer({
|
||||||
rotateEnabled: false,
|
rotateEnabled: false,
|
||||||
keepRatio: false,
|
keepRatio: false,
|
||||||
@ -301,36 +246,39 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
layer.add(transformer);
|
currentPageLayer.add(transformer);
|
||||||
|
|
||||||
// Add selection rectangle.
|
// Add selection rectangle.
|
||||||
const selectionRectangle = new Konva.Rect({
|
const selectionRectangle = new Konva.Rect({
|
||||||
fill: 'rgba(24, 160, 251, 0.3)',
|
fill: 'rgba(24, 160, 251, 0.3)',
|
||||||
visible: false,
|
visible: false,
|
||||||
});
|
});
|
||||||
layer.add(selectionRectangle);
|
currentPageLayer.add(selectionRectangle);
|
||||||
|
|
||||||
let x1: number;
|
let x1: number;
|
||||||
let y1: number;
|
let y1: number;
|
||||||
let x2: number;
|
let x2: number;
|
||||||
let y2: number;
|
let y2: number;
|
||||||
|
|
||||||
stage.on('mousedown touchstart', (e) => {
|
currentStage.on('mousedown touchstart', (e) => {
|
||||||
// do nothing if we mousedown on any shape
|
// do nothing if we mousedown on any shape
|
||||||
if (e.target !== stage) {
|
if (e.target !== currentStage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pointerPosition = stage.getPointerPosition();
|
const pointerPosition = currentStage.getPointerPosition();
|
||||||
|
|
||||||
if (!pointerPosition) {
|
if (!pointerPosition) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
x1 = pointerPosition.x;
|
console.log(`pointerPosition.x: ${pointerPosition.x}`);
|
||||||
y1 = pointerPosition.y;
|
console.log(`pointerPosition.y: ${pointerPosition.y}`);
|
||||||
x2 = pointerPosition.x;
|
|
||||||
y2 = pointerPosition.y;
|
x1 = pointerPosition.x / scale;
|
||||||
|
y1 = pointerPosition.y / scale;
|
||||||
|
x2 = pointerPosition.x / scale;
|
||||||
|
y2 = pointerPosition.y / scale;
|
||||||
|
|
||||||
selectionRectangle.setAttrs({
|
selectionRectangle.setAttrs({
|
||||||
x: x1,
|
x: x1,
|
||||||
@ -341,7 +289,7 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
stage.on('mousemove touchmove', () => {
|
currentStage.on('mousemove touchmove', () => {
|
||||||
// do nothing if we didn't start selection
|
// do nothing if we didn't start selection
|
||||||
if (!selectionRectangle.visible()) {
|
if (!selectionRectangle.visible()) {
|
||||||
return;
|
return;
|
||||||
@ -349,14 +297,14 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
|
|
||||||
selectionRectangle.moveToTop();
|
selectionRectangle.moveToTop();
|
||||||
|
|
||||||
const pointerPosition = stage.getPointerPosition();
|
const pointerPosition = currentStage.getPointerPosition();
|
||||||
|
|
||||||
if (!pointerPosition) {
|
if (!pointerPosition) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
x2 = pointerPosition.x;
|
x2 = pointerPosition.x / scale;
|
||||||
y2 = pointerPosition.y;
|
y2 = pointerPosition.y / scale;
|
||||||
|
|
||||||
selectionRectangle.setAttrs({
|
selectionRectangle.setAttrs({
|
||||||
x: Math.min(x1, x2),
|
x: Math.min(x1, x2),
|
||||||
@ -366,7 +314,7 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
stage.on('mouseup touchend', () => {
|
currentStage.on('mouseup touchend', () => {
|
||||||
// do nothing if we didn't start selection
|
// do nothing if we didn't start selection
|
||||||
if (!selectionRectangle.visible()) {
|
if (!selectionRectangle.visible()) {
|
||||||
return;
|
return;
|
||||||
@ -377,38 +325,41 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
selectionRectangle.visible(false);
|
selectionRectangle.visible(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
const stageFieldGroups = stage.find('.field-group') || [];
|
const stageFieldGroups = currentStage.find('.field-group') || [];
|
||||||
const box = selectionRectangle.getClientRect();
|
const box = selectionRectangle.getClientRect();
|
||||||
const selectedFieldGroups = stageFieldGroups.filter(
|
const selectedFieldGroups = stageFieldGroups.filter(
|
||||||
(shape) => Konva.Util.haveIntersection(box, shape.getClientRect()) && shape.draggable(),
|
(shape) => Konva.Util.haveIntersection(box, shape.getClientRect()) && shape.draggable(),
|
||||||
);
|
);
|
||||||
setSelectedFields(selectedFieldGroups);
|
setSelectedFields(selectedFieldGroups);
|
||||||
|
|
||||||
|
const unscaledBoxWidth = box.width / scale;
|
||||||
|
const unscaledBoxHeight = box.height / scale;
|
||||||
|
|
||||||
// Create a field if no items are selected or the size is too small.
|
// Create a field if no items are selected or the size is too small.
|
||||||
if (
|
if (
|
||||||
selectedFieldGroups.length === 0 &&
|
selectedFieldGroups.length === 0 &&
|
||||||
canvasElement.current &&
|
canvasElement.current &&
|
||||||
box.width > MIN_FIELD_WIDTH_PX &&
|
unscaledBoxWidth > MIN_FIELD_WIDTH_PX &&
|
||||||
box.height > MIN_FIELD_HEIGHT_PX &&
|
unscaledBoxHeight > MIN_FIELD_HEIGHT_PX &&
|
||||||
editorFields.selectedRecipient &&
|
editorFields.selectedRecipient &&
|
||||||
canRecipientFieldsBeModified(editorFields.selectedRecipient, envelope.fields)
|
canRecipientFieldsBeModified(editorFields.selectedRecipient, envelope.fields)
|
||||||
) {
|
) {
|
||||||
const pendingFieldCreation = new Konva.Rect({
|
const pendingFieldCreation = new Konva.Rect({
|
||||||
name: 'pending-field-creation',
|
name: 'pending-field-creation',
|
||||||
x: box.x,
|
x: box.x / scale,
|
||||||
y: box.y,
|
y: box.y / scale,
|
||||||
width: box.width,
|
width: unscaledBoxWidth,
|
||||||
height: box.height,
|
height: unscaledBoxHeight,
|
||||||
fill: 'rgba(24, 160, 251, 0.3)',
|
fill: 'rgba(24, 160, 251, 0.3)',
|
||||||
});
|
});
|
||||||
|
|
||||||
layer.add(pendingFieldCreation);
|
currentPageLayer.add(pendingFieldCreation);
|
||||||
setPendingFieldCreation(pendingFieldCreation);
|
setPendingFieldCreation(pendingFieldCreation);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clicks should select/deselect shapes
|
// Clicks should select/deselect shapes
|
||||||
stage.on('click tap', function (e) {
|
currentStage.on('click tap', function (e) {
|
||||||
// if we are selecting with rect, do nothing
|
// if we are selecting with rect, do nothing
|
||||||
if (
|
if (
|
||||||
selectionRectangle.visible() &&
|
selectionRectangle.visible() &&
|
||||||
@ -419,7 +370,7 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If empty area clicked, remove all selections
|
// If empty area clicked, remove all selections
|
||||||
if (e.target === stage) {
|
if (e.target === stage.current) {
|
||||||
setSelectedFields([]);
|
setSelectedFields([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -555,15 +506,13 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { height: pageHeight, width: pageWidth } = getBoundingClientRect(canvasElement.current);
|
|
||||||
|
|
||||||
const { fieldX, fieldY, fieldWidth, fieldHeight } = convertPixelToPercentage({
|
const { fieldX, fieldY, fieldWidth, fieldHeight } = convertPixelToPercentage({
|
||||||
width: pixelWidth,
|
width: pixelWidth,
|
||||||
height: pixelHeight,
|
height: pixelHeight,
|
||||||
positionX: pixelX,
|
positionX: pixelX,
|
||||||
positionY: pixelY,
|
positionY: pixelY,
|
||||||
pageWidth,
|
pageWidth: unscaledViewport.width,
|
||||||
pageHeight,
|
pageHeight: unscaledViewport.height,
|
||||||
});
|
});
|
||||||
|
|
||||||
editorFields.addField({
|
editorFields.addField({
|
||||||
@ -597,7 +546,10 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative" key={`${currentEnvelopeItem.id}-renderer-${pageContext.pageNumber}`}>
|
<div
|
||||||
|
className="relative w-full"
|
||||||
|
key={`${currentEnvelopeItem.id}-renderer-${pageContext.pageNumber}`}
|
||||||
|
>
|
||||||
{selectedKonvaFieldGroups.length > 0 &&
|
{selectedKonvaFieldGroups.length > 0 &&
|
||||||
interactiveTransformer.current &&
|
interactiveTransformer.current &&
|
||||||
!isFieldChanging && (
|
!isFieldChanging && (
|
||||||
@ -654,8 +606,15 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: pendingFieldCreation.y() + pendingFieldCreation.getClientRect().height + 5 + 'px',
|
top:
|
||||||
left: pendingFieldCreation.x() + pendingFieldCreation.getClientRect().width / 2 + 'px',
|
pendingFieldCreation.y() * scale +
|
||||||
|
pendingFieldCreation.getClientRect().height +
|
||||||
|
5 +
|
||||||
|
'px',
|
||||||
|
left:
|
||||||
|
pendingFieldCreation.x() * scale +
|
||||||
|
pendingFieldCreation.getClientRect().width / 2 +
|
||||||
|
'px',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
zIndex: 50,
|
zIndex: 50,
|
||||||
}}
|
}}
|
||||||
@ -673,13 +632,15 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="konva-container absolute inset-0 z-10" ref={konvaContainer}></div>
|
{/* The element Konva will inject it's canvas into. */}
|
||||||
|
<div className="konva-container absolute inset-0 z-10 w-full" ref={konvaContainer}></div>
|
||||||
|
|
||||||
|
{/* Canvas the PDF will be rendered on. */}
|
||||||
<canvas
|
<canvas
|
||||||
className={`${_className}__canvas z-0`}
|
className={`${_className}__canvas z-0`}
|
||||||
height={viewport.height}
|
|
||||||
ref={canvasElement}
|
ref={canvasElement}
|
||||||
width={viewport.width}
|
height={scaledViewport.height}
|
||||||
|
width={scaledViewport.width}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -60,7 +60,7 @@ const FieldSettingsTypeTranslations: Record<FieldType, MessageDescriptor> = {
|
|||||||
[FieldType.DROPDOWN]: msg`Dropdown Settings`,
|
[FieldType.DROPDOWN]: msg`Dropdown Settings`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EnvelopeEditorPageFields = () => {
|
export const EnvelopeEditorFieldsPage = () => {
|
||||||
const { envelope, editorFields } = useCurrentEnvelopeEditor();
|
const { envelope, editorFields } = useCurrentEnvelopeEditor();
|
||||||
|
|
||||||
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
||||||
@ -109,7 +109,7 @@ export const EnvelopeEditorPageFields = () => {
|
|||||||
<EnvelopeRendererFileSelector fields={editorFields.localFields} />
|
<EnvelopeRendererFileSelector fields={editorFields.localFields} />
|
||||||
|
|
||||||
{/* Document View */}
|
{/* Document View */}
|
||||||
<div className="mt-4 flex justify-center">
|
<div className="mt-4 flex justify-center p-4">
|
||||||
{currentEnvelopeItem !== null ? (
|
{currentEnvelopeItem !== null ? (
|
||||||
<PDFViewerKonvaLazy customPageRenderer={EnvelopeEditorFieldsPageRenderer} />
|
<PDFViewerKonvaLazy customPageRenderer={EnvelopeEditorFieldsPageRenderer} />
|
||||||
) : (
|
) : (
|
||||||
@ -1,176 +0,0 @@
|
|||||||
import { useEffect, useMemo, useRef } from 'react';
|
|
||||||
|
|
||||||
import Konva from 'konva';
|
|
||||||
import type { Layer } from 'konva/lib/Layer';
|
|
||||||
import type { RenderParameters } from 'pdfjs-dist/types/src/display/api';
|
|
||||||
import { usePageContext } from 'react-pdf';
|
|
||||||
|
|
||||||
import type { TLocalField } from '@documenso/lib/client-only/hooks/use-editor-fields';
|
|
||||||
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
|
||||||
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
|
||||||
import { renderField } from '@documenso/lib/universal/field-renderer/render-field';
|
|
||||||
|
|
||||||
export default function EnvelopeEditorPagePreviewRenderer() {
|
|
||||||
const pageContext = usePageContext();
|
|
||||||
|
|
||||||
if (!pageContext) {
|
|
||||||
throw new Error('Unable to find Page context.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { _className, page, rotate, scale } = pageContext;
|
|
||||||
|
|
||||||
if (!page) {
|
|
||||||
throw new Error('Attempted to render page canvas, but no page was specified.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { editorFields, getRecipientColorKey } = useCurrentEnvelopeEditor();
|
|
||||||
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
|
||||||
|
|
||||||
const canvasElement = useRef<HTMLCanvasElement>(null);
|
|
||||||
const konvaContainer = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const stage = useRef<Konva.Stage | null>(null);
|
|
||||||
const pageLayer = useRef<Layer | null>(null);
|
|
||||||
|
|
||||||
const viewport = useMemo(
|
|
||||||
() => page.getViewport({ scale, rotation: rotate }),
|
|
||||||
[page, rotate, scale],
|
|
||||||
);
|
|
||||||
|
|
||||||
const localPageFields = useMemo(
|
|
||||||
() =>
|
|
||||||
editorFields.localFields.filter(
|
|
||||||
(field) =>
|
|
||||||
field.page === pageContext.pageNumber && field.envelopeItemId === currentEnvelopeItem?.id,
|
|
||||||
),
|
|
||||||
[editorFields.localFields, pageContext.pageNumber],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Custom renderer from Konva examples.
|
|
||||||
useEffect(
|
|
||||||
function drawPageOnCanvas() {
|
|
||||||
if (!page) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { current: canvas } = canvasElement;
|
|
||||||
const { current: container } = konvaContainer;
|
|
||||||
|
|
||||||
if (!canvas || !container) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderContext: RenderParameters = {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
canvasContext: canvas.getContext('2d', { alpha: false }) as CanvasRenderingContext2D,
|
|
||||||
viewport,
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancellable = page.render(renderContext);
|
|
||||||
const runningTask = cancellable;
|
|
||||||
|
|
||||||
cancellable.promise.catch(() => {
|
|
||||||
// Intentionally empty
|
|
||||||
});
|
|
||||||
|
|
||||||
void cancellable.promise.then(() => {
|
|
||||||
createPageCanvas(container);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
runningTask.cancel();
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[page, viewport],
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderFieldOnLayer = (field: TLocalField) => {
|
|
||||||
if (!pageLayer.current) {
|
|
||||||
console.error('Layer not loaded yet');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderField({
|
|
||||||
pageLayer: pageLayer.current,
|
|
||||||
field: {
|
|
||||||
renderId: field.formId,
|
|
||||||
...field,
|
|
||||||
customText: '',
|
|
||||||
inserted: false,
|
|
||||||
fieldMeta: field.fieldMeta,
|
|
||||||
},
|
|
||||||
pageWidth: viewport.width,
|
|
||||||
pageHeight: viewport.height,
|
|
||||||
color: getRecipientColorKey(field.recipientId),
|
|
||||||
editable: false,
|
|
||||||
mode: 'export',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the initial Konva page canvas and initialize all fields and interactions.
|
|
||||||
*/
|
|
||||||
const createPageCanvas = (container: HTMLDivElement) => {
|
|
||||||
stage.current = new Konva.Stage({
|
|
||||||
container,
|
|
||||||
width: viewport.width,
|
|
||||||
height: viewport.height,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create the main layer for interactive elements.
|
|
||||||
pageLayer.current = new Konva.Layer();
|
|
||||||
stage.current?.add(pageLayer.current);
|
|
||||||
|
|
||||||
// Render the fields.
|
|
||||||
for (const field of localPageFields) {
|
|
||||||
renderFieldOnLayer(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
pageLayer.current.batchDraw();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render fields when they are added or removed from the localFields.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (!pageLayer.current || !stage.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If doesn't exist in localFields, destroy it since it's been deleted.
|
|
||||||
pageLayer.current.find('Group').forEach((group) => {
|
|
||||||
if (
|
|
||||||
group.name() === 'field-group' &&
|
|
||||||
!localPageFields.some((field) => field.formId === group.id())
|
|
||||||
) {
|
|
||||||
console.log('Field removed, removing from canvas');
|
|
||||||
group.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// If it exists, rerender.
|
|
||||||
localPageFields.forEach((field) => {
|
|
||||||
console.log('Field created/updated, rendering on canvas');
|
|
||||||
renderFieldOnLayer(field);
|
|
||||||
});
|
|
||||||
|
|
||||||
pageLayer.current.batchDraw();
|
|
||||||
}, [localPageFields]);
|
|
||||||
|
|
||||||
if (!currentEnvelopeItem) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative" key={`${currentEnvelopeItem.id}-renderer-${pageContext.pageNumber}`}>
|
|
||||||
<div className="konva-container absolute inset-0 z-10" ref={konvaContainer}></div>
|
|
||||||
|
|
||||||
<canvas
|
|
||||||
className={`${_className}__canvas z-0`}
|
|
||||||
height={viewport.height}
|
|
||||||
ref={canvasElement}
|
|
||||||
width={viewport.width}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -13,11 +13,9 @@ import { Separator } from '@documenso/ui/primitives/separator';
|
|||||||
|
|
||||||
import { EnvelopeRendererFileSelector } from './envelope-file-selector';
|
import { EnvelopeRendererFileSelector } from './envelope-file-selector';
|
||||||
|
|
||||||
const EnvelopeEditorPagePreviewRenderer = lazy(
|
const EnvelopeGenericPageRenderer = lazy(async () => import('./envelope-generic-page-renderer'));
|
||||||
async () => import('./envelope-editor-page-preview-renderer'),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const EnvelopeEditorPagePreview = () => {
|
export const EnvelopeEditorPreviewPage = () => {
|
||||||
const { envelope, editorFields } = useCurrentEnvelopeEditor();
|
const { envelope, editorFields } = useCurrentEnvelopeEditor();
|
||||||
|
|
||||||
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
||||||
@ -51,7 +49,7 @@ export const EnvelopeEditorPagePreview = () => {
|
|||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
{currentEnvelopeItem !== null ? (
|
{currentEnvelopeItem !== null ? (
|
||||||
<PDFViewerKonvaLazy customPageRenderer={EnvelopeEditorPagePreviewRenderer} />
|
<PDFViewerKonvaLazy customPageRenderer={EnvelopeGenericPageRenderer} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center py-32">
|
<div className="flex flex-col items-center justify-center py-32">
|
||||||
<FileTextIcon className="text-muted-foreground h-10 w-10" />
|
<FileTextIcon className="text-muted-foreground h-10 w-10" />
|
||||||
@ -41,7 +41,7 @@ type LocalFile = {
|
|||||||
isError: boolean;
|
isError: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EnvelopeEditorPageUpload = () => {
|
export const EnvelopeEditorUploadPage = () => {
|
||||||
const team = useCurrentTeam();
|
const team = useCurrentTeam();
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
@ -224,8 +224,12 @@ export const EnvelopeEditorPageUpload = () => {
|
|||||||
<div className="mx-auto max-w-4xl space-y-6 p-8">
|
<div className="mx-auto max-w-4xl space-y-6 p-8">
|
||||||
<Card backdropBlur={false} className="border">
|
<Card backdropBlur={false} className="border">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardTitle>Documents</CardTitle>
|
<CardTitle>
|
||||||
<CardDescription>Add and configure multiple documents</CardDescription>
|
<Trans>Documents</Trans>
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
<Trans>Add and configure multiple documents</Trans>
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@ -39,10 +39,10 @@ import { TemplateDirectLinkDialog } from '~/components/dialogs/template-direct-l
|
|||||||
import { EnvelopeEditorSettingsDialog } from '~/components/general/envelope-editor/envelope-editor-settings-dialog';
|
import { EnvelopeEditorSettingsDialog } from '~/components/general/envelope-editor/envelope-editor-settings-dialog';
|
||||||
import { useCurrentTeam } from '~/providers/team';
|
import { useCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
|
import { EnvelopeEditorFieldsPage } from './envelope-editor-fields-page';
|
||||||
import EnvelopeEditorHeader from './envelope-editor-header';
|
import EnvelopeEditorHeader from './envelope-editor-header';
|
||||||
import { EnvelopeEditorPageFields } from './envelope-editor-page-fields';
|
import { EnvelopeEditorPreviewPage } from './envelope-editor-preview-page';
|
||||||
import { EnvelopeEditorPagePreview } from './envelope-editor-page-preview';
|
import { EnvelopeEditorUploadPage } from './envelope-editor-upload-page';
|
||||||
import { EnvelopeEditorPageUpload } from './envelope-editor-page-upload';
|
|
||||||
|
|
||||||
type EnvelopeEditorStep = 'upload' | 'addFields' | 'preview';
|
type EnvelopeEditorStep = 'upload' | 'addFields' | 'preview';
|
||||||
|
|
||||||
@ -128,6 +128,18 @@ export default function EnvelopeEditor() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Watch the URL params and setStep if the step changes.
|
||||||
|
useEffect(() => {
|
||||||
|
const stepParam = searchParams.get('step') || envelopeEditorSteps[0].id;
|
||||||
|
|
||||||
|
const foundStep = envelopeEditorSteps.find((step) => step.id === stepParam);
|
||||||
|
|
||||||
|
if (foundStep && foundStep.id !== currentStep) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
navigateToStep(foundStep.id as EnvelopeEditorStep);
|
||||||
|
}
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAutosaving) {
|
if (!isAutosaving) {
|
||||||
setIsStepLoading(false);
|
setIsStepLoading(false);
|
||||||
@ -151,7 +163,9 @@ export default function EnvelopeEditor() {
|
|||||||
{isDocument ? <Trans>Document Editor</Trans> : <Trans>Template Editor</Trans>}
|
{isDocument ? <Trans>Document Editor</Trans> : <Trans>Template Editor</Trans>}
|
||||||
|
|
||||||
<span className="text-muted-foreground ml-2 rounded border bg-gray-50 px-2 py-0.5 text-xs">
|
<span className="text-muted-foreground ml-2 rounded border bg-gray-50 px-2 py-0.5 text-xs">
|
||||||
|
<Trans context="The step counter">
|
||||||
Step {currentStepData.order}/{envelopeEditorSteps.length}
|
Step {currentStepData.order}/{envelopeEditorSteps.length}
|
||||||
|
</Trans>
|
||||||
</span>
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
@ -340,13 +354,12 @@ export default function EnvelopeEditor() {
|
|||||||
|
|
||||||
{/* Main Content - Changes based on current step */}
|
{/* Main Content - Changes based on current step */}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<p>{isAutosaving ? 'Autosaving...' : 'Not autosaving'}</p>
|
|
||||||
<AnimateGenericFadeInOut key={currentStep}>
|
<AnimateGenericFadeInOut key={currentStep}>
|
||||||
{match({ currentStep, isStepLoading })
|
{match({ currentStep, isStepLoading })
|
||||||
.with({ isStepLoading: true }, () => <SpinnerBox className="py-32" />)
|
.with({ isStepLoading: true }, () => <SpinnerBox className="py-32" />)
|
||||||
.with({ currentStep: 'upload' }, () => <EnvelopeEditorPageUpload />)
|
.with({ currentStep: 'upload' }, () => <EnvelopeEditorUploadPage />)
|
||||||
.with({ currentStep: 'addFields' }, () => <EnvelopeEditorPageFields />)
|
.with({ currentStep: 'addFields' }, () => <EnvelopeEditorFieldsPage />)
|
||||||
.with({ currentStep: 'preview' }, () => <EnvelopeEditorPagePreview />)
|
.with({ currentStep: 'preview' }, () => <EnvelopeEditorPreviewPage />)
|
||||||
.exhaustive()}
|
.exhaustive()}
|
||||||
</AnimateGenericFadeInOut>
|
</AnimateGenericFadeInOut>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,41 +1,31 @@
|
|||||||
import { useEffect, useMemo, useRef } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import Konva from 'konva';
|
import type Konva from 'konva';
|
||||||
import type { Layer } from 'konva/lib/Layer';
|
|
||||||
import type { RenderParameters } from 'pdfjs-dist/types/src/display/api';
|
|
||||||
import { usePageContext } from 'react-pdf';
|
|
||||||
|
|
||||||
|
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
|
||||||
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||||
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||||
import { renderField } from '@documenso/lib/universal/field-renderer/render-field';
|
import { renderField } from '@documenso/lib/universal/field-renderer/render-field';
|
||||||
|
|
||||||
export default function EnvelopeGenericPageRenderer() {
|
export default function EnvelopeGenericPageRenderer() {
|
||||||
const pageContext = usePageContext();
|
|
||||||
|
|
||||||
if (!pageContext) {
|
|
||||||
throw new Error('Unable to find Page context.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { _className, page, rotate, scale } = pageContext;
|
|
||||||
|
|
||||||
if (!page) {
|
|
||||||
throw new Error('Attempted to render page canvas, but no page was specified.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
const { currentEnvelopeItem, fields } = useCurrentEnvelopeRender();
|
const { currentEnvelopeItem, fields } = useCurrentEnvelopeRender();
|
||||||
|
|
||||||
const canvasElement = useRef<HTMLCanvasElement>(null);
|
const {
|
||||||
const konvaContainer = useRef<HTMLDivElement>(null);
|
stage,
|
||||||
|
pageLayer,
|
||||||
|
canvasElement,
|
||||||
|
konvaContainer,
|
||||||
|
pageContext,
|
||||||
|
scaledViewport,
|
||||||
|
unscaledViewport,
|
||||||
|
} = usePageRenderer(({ stage, pageLayer }) => {
|
||||||
|
createPageCanvas(stage, pageLayer);
|
||||||
|
});
|
||||||
|
|
||||||
const stage = useRef<Konva.Stage | null>(null);
|
const { _className, scale } = pageContext;
|
||||||
const pageLayer = useRef<Layer | null>(null);
|
|
||||||
|
|
||||||
const viewport = useMemo(
|
|
||||||
() => page.getViewport({ scale, rotation: rotate }),
|
|
||||||
[page, rotate, scale],
|
|
||||||
);
|
|
||||||
|
|
||||||
const localPageFields = useMemo(
|
const localPageFields = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -46,44 +36,6 @@ export default function EnvelopeGenericPageRenderer() {
|
|||||||
[fields, pageContext.pageNumber],
|
[fields, pageContext.pageNumber],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Custom renderer from Konva examples.
|
|
||||||
useEffect(
|
|
||||||
function drawPageOnCanvas() {
|
|
||||||
if (!page) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { current: canvas } = canvasElement;
|
|
||||||
const { current: container } = konvaContainer;
|
|
||||||
|
|
||||||
if (!canvas || !container) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderContext: RenderParameters = {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
canvasContext: canvas.getContext('2d', { alpha: false }) as CanvasRenderingContext2D,
|
|
||||||
viewport,
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancellable = page.render(renderContext);
|
|
||||||
const runningTask = cancellable;
|
|
||||||
|
|
||||||
cancellable.promise.catch(() => {
|
|
||||||
// Intentionally empty
|
|
||||||
});
|
|
||||||
|
|
||||||
void cancellable.promise.then(() => {
|
|
||||||
createPageCanvas(container);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
runningTask.cancel();
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[page, viewport],
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderFieldOnLayer = (field: TEnvelope['fields'][number]) => {
|
const renderFieldOnLayer = (field: TEnvelope['fields'][number]) => {
|
||||||
if (!pageLayer.current) {
|
if (!pageLayer.current) {
|
||||||
console.error('Layer not loaded yet');
|
console.error('Layer not loaded yet');
|
||||||
@ -91,6 +43,7 @@ export default function EnvelopeGenericPageRenderer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderField({
|
renderField({
|
||||||
|
scale,
|
||||||
pageLayer: pageLayer.current,
|
pageLayer: pageLayer.current,
|
||||||
field: {
|
field: {
|
||||||
renderId: field.id.toString(),
|
renderId: field.id.toString(),
|
||||||
@ -103,8 +56,8 @@ export default function EnvelopeGenericPageRenderer() {
|
|||||||
inserted: false,
|
inserted: false,
|
||||||
fieldMeta: field.fieldMeta,
|
fieldMeta: field.fieldMeta,
|
||||||
},
|
},
|
||||||
pageWidth: viewport.width,
|
pageWidth: unscaledViewport.width,
|
||||||
pageHeight: viewport.height,
|
pageHeight: unscaledViewport.height,
|
||||||
// color: getRecipientColorKey(field.recipientId),
|
// color: getRecipientColorKey(field.recipientId),
|
||||||
color: 'purple', // Todo
|
color: 'purple', // Todo
|
||||||
editable: false,
|
editable: false,
|
||||||
@ -113,25 +66,15 @@ export default function EnvelopeGenericPageRenderer() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the initial Konva page canvas and initialize all fields and interactions.
|
* Initialize the Konva page canvas and all fields and interactions.
|
||||||
*/
|
*/
|
||||||
const createPageCanvas = (container: HTMLDivElement) => {
|
const createPageCanvas = (_currentStage: Konva.Stage, currentPageLayer: Konva.Layer) => {
|
||||||
stage.current = new Konva.Stage({
|
|
||||||
container,
|
|
||||||
width: viewport.width,
|
|
||||||
height: viewport.height,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create the main layer for interactive elements.
|
|
||||||
pageLayer.current = new Konva.Layer();
|
|
||||||
stage.current?.add(pageLayer.current);
|
|
||||||
|
|
||||||
// Render the fields.
|
// Render the fields.
|
||||||
for (const field of localPageFields) {
|
for (const field of localPageFields) {
|
||||||
renderFieldOnLayer(field);
|
renderFieldOnLayer(field);
|
||||||
}
|
}
|
||||||
|
|
||||||
pageLayer.current.batchDraw();
|
currentPageLayer.batchDraw();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -167,14 +110,19 @@ export default function EnvelopeGenericPageRenderer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative" key={`${currentEnvelopeItem.id}-renderer-${pageContext.pageNumber}`}>
|
<div
|
||||||
<div className="konva-container absolute inset-0 z-10" ref={konvaContainer}></div>
|
className="relative w-full"
|
||||||
|
key={`${currentEnvelopeItem.id}-renderer-${pageContext.pageNumber}`}
|
||||||
|
>
|
||||||
|
{/* The element Konva will inject it's canvas into. */}
|
||||||
|
<div className="konva-container absolute inset-0 z-10 w-full" ref={konvaContainer}></div>
|
||||||
|
|
||||||
|
{/* Canvas the PDF will be rendered on. */}
|
||||||
<canvas
|
<canvas
|
||||||
className={`${_className}__canvas z-0`}
|
className={`${_className}__canvas z-0`}
|
||||||
height={viewport.height}
|
|
||||||
ref={canvasElement}
|
ref={canvasElement}
|
||||||
width={viewport.width}
|
height={scaledViewport.height}
|
||||||
|
width={scaledViewport.width}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
import { useEffect, useMemo, useRef } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { type Field, FieldType } from '@prisma/client';
|
import { type Field, FieldType, type Signature } from '@prisma/client';
|
||||||
import Konva from 'konva';
|
import type Konva from 'konva';
|
||||||
import type { Layer } from 'konva/lib/Layer';
|
|
||||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||||
import type { RenderParameters } from 'pdfjs-dist/types/src/display/api';
|
|
||||||
import { usePageContext } from 'react-pdf';
|
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
|
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
|
||||||
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||||
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';
|
||||||
@ -28,18 +26,6 @@ import { handleTextFieldClick } from '~/utils/field-signing/text-field';
|
|||||||
import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider';
|
import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider';
|
||||||
|
|
||||||
export default function EnvelopeSignerPageRenderer() {
|
export default function EnvelopeSignerPageRenderer() {
|
||||||
const pageContext = usePageContext();
|
|
||||||
|
|
||||||
if (!pageContext) {
|
|
||||||
throw new Error('Unable to find Page context.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { _className, page, rotate, scale } = pageContext;
|
|
||||||
|
|
||||||
if (!page) {
|
|
||||||
throw new Error('Attempted to render page canvas, but no page was specified.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
||||||
@ -58,21 +44,20 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
setSignature,
|
setSignature,
|
||||||
} = useRequiredEnvelopeSigningContext();
|
} = useRequiredEnvelopeSigningContext();
|
||||||
|
|
||||||
console.log({ fullName });
|
const {
|
||||||
|
stage,
|
||||||
|
pageLayer,
|
||||||
|
canvasElement,
|
||||||
|
konvaContainer,
|
||||||
|
pageContext,
|
||||||
|
scaledViewport,
|
||||||
|
unscaledViewport,
|
||||||
|
} = usePageRenderer(({ stage, pageLayer }) => createPageCanvas(stage, pageLayer));
|
||||||
|
|
||||||
|
const { _className, scale } = pageContext;
|
||||||
|
|
||||||
const { envelope } = envelopeData;
|
const { envelope } = envelopeData;
|
||||||
|
|
||||||
const canvasElement = useRef<HTMLCanvasElement>(null);
|
|
||||||
const konvaContainer = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const stage = useRef<Konva.Stage | null>(null);
|
|
||||||
const pageLayer = useRef<Layer | null>(null);
|
|
||||||
|
|
||||||
const viewport = useMemo(
|
|
||||||
() => page.getViewport({ scale, rotation: rotate }),
|
|
||||||
[page, rotate, scale],
|
|
||||||
);
|
|
||||||
|
|
||||||
const localPageFields = useMemo(
|
const localPageFields = useMemo(
|
||||||
() =>
|
() =>
|
||||||
recipientFields.filter(
|
recipientFields.filter(
|
||||||
@ -82,45 +67,7 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
[recipientFields, pageContext.pageNumber],
|
[recipientFields, pageContext.pageNumber],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Custom renderer from Konva examples.
|
const renderFieldOnLayer = (unparsedField: Field & { signature?: Signature | null }) => {
|
||||||
useEffect(
|
|
||||||
function drawPageOnCanvas() {
|
|
||||||
if (!page) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { current: canvas } = canvasElement;
|
|
||||||
const { current: container } = konvaContainer;
|
|
||||||
|
|
||||||
if (!canvas || !container) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderContext: RenderParameters = {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
canvasContext: canvas.getContext('2d', { alpha: false }) as CanvasRenderingContext2D,
|
|
||||||
viewport,
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancellable = page.render(renderContext);
|
|
||||||
const runningTask = cancellable;
|
|
||||||
|
|
||||||
cancellable.promise.catch(() => {
|
|
||||||
// Intentionally empty
|
|
||||||
});
|
|
||||||
|
|
||||||
void cancellable.promise.then(() => {
|
|
||||||
createPageCanvas(container);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
runningTask.cancel();
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[page, viewport],
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderFieldOnLayer = (unparsedField: Field) => {
|
|
||||||
if (!pageLayer.current) {
|
if (!pageLayer.current) {
|
||||||
console.error('Layer not loaded yet');
|
console.error('Layer not loaded yet');
|
||||||
return;
|
return;
|
||||||
@ -137,6 +84,7 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { fieldGroup } = renderField({
|
const { fieldGroup } = renderField({
|
||||||
|
scale,
|
||||||
pageLayer: pageLayer.current,
|
pageLayer: pageLayer.current,
|
||||||
field: {
|
field: {
|
||||||
renderId: fieldToRender.id.toString(),
|
renderId: fieldToRender.id.toString(),
|
||||||
@ -145,9 +93,10 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
height: Number(fieldToRender.height),
|
height: Number(fieldToRender.height),
|
||||||
positionX: Number(fieldToRender.positionX),
|
positionX: Number(fieldToRender.positionX),
|
||||||
positionY: Number(fieldToRender.positionY),
|
positionY: Number(fieldToRender.positionY),
|
||||||
|
signature: unparsedField.signature,
|
||||||
},
|
},
|
||||||
pageWidth: viewport.width,
|
pageWidth: unscaledViewport.width,
|
||||||
pageHeight: viewport.height,
|
pageHeight: unscaledViewport.height,
|
||||||
color,
|
color,
|
||||||
mode: 'sign',
|
mode: 'sign',
|
||||||
});
|
});
|
||||||
@ -357,29 +306,19 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the initial Konva page canvas and initialize all fields and interactions.
|
* Initialize the Konva page canvas and all fields and interactions.
|
||||||
*/
|
*/
|
||||||
const createPageCanvas = (container: HTMLDivElement) => {
|
const createPageCanvas = (currentStage: Konva.Stage, currentPageLayer: Konva.Layer) => {
|
||||||
stage.current = new Konva.Stage({
|
|
||||||
container,
|
|
||||||
width: viewport.width,
|
|
||||||
height: viewport.height,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create the main layer for interactive elements.
|
|
||||||
pageLayer.current = new Konva.Layer();
|
|
||||||
stage.current?.add(pageLayer.current);
|
|
||||||
|
|
||||||
console.log({
|
console.log({
|
||||||
localPageFields,
|
localPageFields,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Render the fields.
|
// Render the fields.
|
||||||
for (const field of localPageFields) {
|
for (const field of localPageFields) {
|
||||||
renderFieldOnLayer(field);
|
renderFieldOnLayer(field); // Todo: Envelopes - [CRITICAL] Handle errors which prevent rendering
|
||||||
}
|
}
|
||||||
|
|
||||||
pageLayer.current.batchDraw();
|
currentPageLayer.batchDraw();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -392,7 +331,7 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
|
|
||||||
localPageFields.forEach((field) => {
|
localPageFields.forEach((field) => {
|
||||||
console.log('Field changed/inserted, rendering on canvas');
|
console.log('Field changed/inserted, rendering on canvas');
|
||||||
renderFieldOnLayer(field);
|
renderFieldOnLayer(field); // Todo: Envelopes - [CRITICAL] Handle errors which prevent rendering
|
||||||
});
|
});
|
||||||
|
|
||||||
pageLayer.current.batchDraw();
|
pageLayer.current.batchDraw();
|
||||||
@ -403,14 +342,19 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative" key={`${currentEnvelopeItem.id}-renderer-${pageContext.pageNumber}`}>
|
<div
|
||||||
|
className="relative w-full"
|
||||||
|
key={`${currentEnvelopeItem.id}-renderer-${pageContext.pageNumber}`}
|
||||||
|
>
|
||||||
|
{/* 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>
|
||||||
|
|
||||||
|
{/* Canvas the PDF will be rendered on. */}
|
||||||
<canvas
|
<canvas
|
||||||
className={`${_className}__canvas z-0`}
|
className={`${_className}__canvas z-0`}
|
||||||
height={viewport.height}
|
|
||||||
ref={canvasElement}
|
ref={canvasElement}
|
||||||
width={viewport.width}
|
height={scaledViewport.height}
|
||||||
|
width={scaledViewport.width}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -19,6 +19,8 @@ import { DocumentUploadButton } from '~/components/general/document/document-upl
|
|||||||
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 '../document/envelope-upload-button';
|
||||||
|
|
||||||
export type FolderGridProps = {
|
export type FolderGridProps = {
|
||||||
type: FolderType;
|
type: FolderType;
|
||||||
parentId: string | null;
|
parentId: string | null;
|
||||||
@ -98,7 +100,7 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
|
|||||||
|
|
||||||
<div className="flex gap-4 sm:flex-row sm:justify-end">
|
<div className="flex gap-4 sm:flex-row sm:justify-end">
|
||||||
{/* Todo: Envelopes - Feature flag */}
|
{/* Todo: Envelopes - Feature flag */}
|
||||||
{/* <EnvelopeUploadButton type={type} folderId={parentId || undefined} /> */}
|
<EnvelopeUploadButton type={type} folderId={parentId || undefined} />
|
||||||
|
|
||||||
{type === FolderType.DOCUMENT ? (
|
{type === FolderType.DOCUMENT ? (
|
||||||
<DocumentUploadButton />
|
<DocumentUploadButton />
|
||||||
|
|||||||
@ -136,6 +136,7 @@ export const useEditorFields = ({
|
|||||||
const field: TLocalField = {
|
const field: TLocalField = {
|
||||||
...fieldData,
|
...fieldData,
|
||||||
formId: nanoid(12),
|
formId: nanoid(12),
|
||||||
|
...restrictFieldPosValues(fieldData),
|
||||||
};
|
};
|
||||||
|
|
||||||
append(field);
|
append(field);
|
||||||
@ -165,7 +166,15 @@ export const useEditorFields = ({
|
|||||||
const index = localFields.findIndex((field) => field.formId === formId);
|
const index = localFields.findIndex((field) => field.formId === formId);
|
||||||
|
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
update(index, { ...localFields[index], ...updates });
|
const updatedField = {
|
||||||
|
...localFields[index],
|
||||||
|
...updates,
|
||||||
|
};
|
||||||
|
|
||||||
|
update(index, {
|
||||||
|
...updatedField,
|
||||||
|
...restrictFieldPosValues(updatedField),
|
||||||
|
});
|
||||||
triggerFieldsUpdate();
|
triggerFieldsUpdate();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -279,3 +288,14 @@ export const useEditorFields = ({
|
|||||||
setSelectedRecipient,
|
setSelectedRecipient,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const restrictFieldPosValues = (
|
||||||
|
field: Pick<TLocalField, 'positionX' | 'positionY' | 'width' | 'height'>,
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
positionX: Math.max(0, Math.min(100, field.positionX)),
|
||||||
|
positionY: Math.max(0, Math.min(100, field.positionY)),
|
||||||
|
width: Math.max(0, Math.min(100, field.width)),
|
||||||
|
height: Math.max(0, Math.min(100, field.height)),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
105
packages/lib/client-only/hooks/use-page-renderer.ts
Normal file
105
packages/lib/client-only/hooks/use-page-renderer.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
|
import Konva from 'konva';
|
||||||
|
import type { RenderParameters } from 'pdfjs-dist/types/src/display/api';
|
||||||
|
import { usePageContext } from 'react-pdf';
|
||||||
|
|
||||||
|
type RenderFunction = (props: { stage: Konva.Stage; pageLayer: Konva.Layer }) => void;
|
||||||
|
|
||||||
|
export function usePageRenderer(renderFunction: RenderFunction) {
|
||||||
|
const pageContext = usePageContext();
|
||||||
|
|
||||||
|
if (!pageContext) {
|
||||||
|
throw new Error('Unable to find Page context.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { page, rotate, scale } = pageContext;
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
throw new Error('Attempted to render page canvas, but no page was specified.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvasElement = useRef<HTMLCanvasElement>(null);
|
||||||
|
const konvaContainer = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const stage = useRef<Konva.Stage | null>(null);
|
||||||
|
const pageLayer = useRef<Konva.Layer | null>(null);
|
||||||
|
|
||||||
|
const unscaledViewport = useMemo(
|
||||||
|
() => page.getViewport({ scale: 1, rotation: rotate }),
|
||||||
|
[page, rotate, scale],
|
||||||
|
);
|
||||||
|
|
||||||
|
const scaledViewport = useMemo(
|
||||||
|
() => page.getViewport({ scale, rotation: rotate }),
|
||||||
|
[page, rotate, scale],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the PDF and create the scaled Konva stage.
|
||||||
|
*/
|
||||||
|
useEffect(
|
||||||
|
function drawPageOnCanvas() {
|
||||||
|
if (!page) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { current: canvas } = canvasElement;
|
||||||
|
const { current: kContainer } = konvaContainer;
|
||||||
|
|
||||||
|
if (!canvas || !kContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderContext: RenderParameters = {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
canvasContext: canvas.getContext('2d', { alpha: false }) as CanvasRenderingContext2D,
|
||||||
|
viewport: scaledViewport,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancellable = page.render(renderContext);
|
||||||
|
const runningTask = cancellable;
|
||||||
|
|
||||||
|
cancellable.promise.catch(() => {
|
||||||
|
// Intentionally empty
|
||||||
|
});
|
||||||
|
|
||||||
|
void cancellable.promise.then(() => {
|
||||||
|
stage.current = new Konva.Stage({
|
||||||
|
container: kContainer,
|
||||||
|
width: scaledViewport.width,
|
||||||
|
height: scaledViewport.height,
|
||||||
|
scale: {
|
||||||
|
x: scale,
|
||||||
|
y: scale,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the main layer for interactive elements.
|
||||||
|
pageLayer.current = new Konva.Layer();
|
||||||
|
|
||||||
|
stage.current.add(pageLayer.current);
|
||||||
|
|
||||||
|
renderFunction({
|
||||||
|
stage: stage.current,
|
||||||
|
pageLayer: pageLayer.current,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
runningTask.cancel();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[page, scaledViewport],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
canvasElement,
|
||||||
|
konvaContainer,
|
||||||
|
stage,
|
||||||
|
pageLayer,
|
||||||
|
unscaledViewport,
|
||||||
|
scaledViewport,
|
||||||
|
pageContext,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -135,7 +135,12 @@ export const EnvelopeEditorProvider = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const envelopeRecipientSetMutationQuery = trpc.envelope.recipient.set.useMutation({
|
const envelopeRecipientSetMutationQuery = trpc.envelope.recipient.set.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: ({ recipients }) => {
|
||||||
|
setEnvelope((prev) => ({
|
||||||
|
...prev,
|
||||||
|
recipients,
|
||||||
|
}));
|
||||||
|
|
||||||
setAutosaveError(false);
|
setAutosaveError(false);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@ -215,14 +220,15 @@ export const EnvelopeEditorProvider = ({
|
|||||||
|
|
||||||
const getRecipientColorKey = useCallback(
|
const getRecipientColorKey = useCallback(
|
||||||
(recipientId: number) => {
|
(recipientId: number) => {
|
||||||
// Todo: Envelopes - Local recipients
|
|
||||||
const recipientIndex = envelope.recipients.findIndex(
|
const recipientIndex = envelope.recipients.findIndex(
|
||||||
(recipient) => recipient.id === recipientId,
|
(recipient) => recipient.id === recipientId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return AVAILABLE_RECIPIENT_COLORS[Math.max(recipientIndex, 0)];
|
return AVAILABLE_RECIPIENT_COLORS[
|
||||||
|
Math.max(recipientIndex, 0) % AVAILABLE_RECIPIENT_COLORS.length
|
||||||
|
];
|
||||||
},
|
},
|
||||||
[envelope.recipients], // Todo: Envelopes - Local recipients
|
[envelope.recipients],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { refetch: reloadEnvelope, isLoading: isReloadingEnvelope } = trpc.envelope.get.useQuery(
|
const { refetch: reloadEnvelope, isLoading: isReloadingEnvelope } = trpc.envelope.get.useQuery(
|
||||||
|
|||||||
@ -87,7 +87,6 @@ export const sendCompletedEmail = async ({ id, requestMetadata }: SendDocumentOp
|
|||||||
return {
|
return {
|
||||||
fileName: document.title.endsWith('.pdf') ? document.title : document.title + '.pdf',
|
fileName: document.title.endsWith('.pdf') ? document.title : document.title + '.pdf',
|
||||||
content: Buffer.from(file),
|
content: Buffer.from(file),
|
||||||
contentType: 'application/pdf',
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -56,6 +56,7 @@ export const sendDocument = async ({
|
|||||||
recipients: {
|
recipients: {
|
||||||
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
|
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
|
||||||
},
|
},
|
||||||
|
fields: true,
|
||||||
documentMeta: true,
|
documentMeta: true,
|
||||||
envelopeItems: {
|
envelopeItems: {
|
||||||
select: {
|
select: {
|
||||||
@ -165,6 +166,16 @@ export const sendDocument = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fieldsToAutoInsert = [];
|
||||||
|
|
||||||
|
// Todo: Envelopes - Handle auto-signing
|
||||||
|
if (envelope.internalVersion === 2) {
|
||||||
|
// fieldsToAutoInsert = envelope.fields.filter((field) => !field.inserted);
|
||||||
|
// if (fieldsToAutoInsert.length > 0) {
|
||||||
|
// //
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
const updatedEnvelope = await prisma.$transaction(async (tx) => {
|
const updatedEnvelope = await prisma.$transaction(async (tx) => {
|
||||||
if (envelope.status === DocumentStatus.DRAFT) {
|
if (envelope.status === DocumentStatus.DRAFT) {
|
||||||
await tx.documentAuditLog.create({
|
await tx.documentAuditLog.create({
|
||||||
|
|||||||
@ -156,9 +156,11 @@ export const setFieldsForDocument = async ({
|
|||||||
|
|
||||||
if (field.type === FieldType.NUMBER && field.fieldMeta) {
|
if (field.type === FieldType.NUMBER && field.fieldMeta) {
|
||||||
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
|
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
|
||||||
|
|
||||||
const errors = validateNumberField(
|
const errors = validateNumberField(
|
||||||
String(numberFieldParsedMeta.value),
|
String(numberFieldParsedMeta.value),
|
||||||
numberFieldParsedMeta,
|
numberFieldParsedMeta,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
|
|||||||
@ -331,57 +331,18 @@ export const insertFieldInPDFV1 = async (pdf: PDFDocument, field: FieldWithSigna
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const selected = field.customText.split(',');
|
const selected = field.customText.split(',');
|
||||||
const direction = meta.data.direction ?? 'vertical';
|
|
||||||
|
|
||||||
const topPadding = 12;
|
const topPadding = 12;
|
||||||
const leftRadioPadding = 8;
|
const leftRadioPadding = 8;
|
||||||
const leftRadioLabelPadding = 12;
|
const leftRadioLabelPadding = 12;
|
||||||
const radioSpaceY = 13;
|
const radioSpaceY = 13;
|
||||||
|
|
||||||
if (direction === 'horizontal') {
|
|
||||||
let currentX = leftRadioPadding;
|
|
||||||
let currentY = topPadding;
|
|
||||||
const maxWidth = pageWidth - fieldX - leftRadioPadding * 2;
|
|
||||||
|
|
||||||
for (const [index, item] of (values ?? []).entries()) {
|
|
||||||
const radio = pdf.getForm().createRadioGroup(`radio.${field.secondaryId}.${index}`);
|
|
||||||
|
|
||||||
const labelText = item.value.includes('empty-value-') ? '' : item.value;
|
|
||||||
const labelWidth = font.widthOfTextAtSize(labelText, 12);
|
|
||||||
const itemWidth = leftRadioLabelPadding + labelWidth + 16;
|
|
||||||
|
|
||||||
if (currentX + itemWidth > maxWidth && index > 0) {
|
|
||||||
currentX = leftRadioPadding;
|
|
||||||
currentY += radioSpaceY;
|
|
||||||
}
|
|
||||||
|
|
||||||
page.drawText(labelText, {
|
|
||||||
x: fieldX + currentX + leftRadioLabelPadding,
|
|
||||||
y: pageHeight - (fieldY + currentY),
|
|
||||||
size: 12,
|
|
||||||
font,
|
|
||||||
rotate: degrees(pageRotationInDegrees),
|
|
||||||
});
|
|
||||||
|
|
||||||
radio.addOptionToPage(item.value, page, {
|
|
||||||
x: fieldX + currentX,
|
|
||||||
y: pageHeight - (fieldY + currentY),
|
|
||||||
height: 8,
|
|
||||||
width: 8,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (selected.includes(item.value)) {
|
|
||||||
radio.select(item.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentX += itemWidth;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const [index, item] of (values ?? []).entries()) {
|
for (const [index, item] of (values ?? []).entries()) {
|
||||||
const offsetY = index * radioSpaceY + topPadding;
|
const offsetY = index * radioSpaceY + topPadding;
|
||||||
|
|
||||||
const radio = pdf.getForm().createRadioGroup(`radio.${field.secondaryId}.${index}`);
|
const radio = pdf.getForm().createRadioGroup(`radio.${field.secondaryId}.${index}`);
|
||||||
|
|
||||||
|
// Draw label.
|
||||||
page.drawText(item.value.includes('empty-value-') ? '' : item.value, {
|
page.drawText(item.value.includes('empty-value-') ? '' : item.value, {
|
||||||
x: fieldX + leftRadioPadding + leftRadioLabelPadding,
|
x: fieldX + leftRadioPadding + leftRadioLabelPadding,
|
||||||
y: pageHeight - (fieldY + offsetY),
|
y: pageHeight - (fieldY + offsetY),
|
||||||
@ -390,6 +351,7 @@ export const insertFieldInPDFV1 = async (pdf: PDFDocument, field: FieldWithSigna
|
|||||||
rotate: degrees(pageRotationInDegrees),
|
rotate: degrees(pageRotationInDegrees),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Draw radio button.
|
||||||
radio.addOptionToPage(item.value, page, {
|
radio.addOptionToPage(item.value, page, {
|
||||||
x: fieldX + leftRadioPadding,
|
x: fieldX + leftRadioPadding,
|
||||||
y: pageHeight - (fieldY + offsetY),
|
y: pageHeight - (fieldY + offsetY),
|
||||||
@ -401,7 +363,6 @@ export const insertFieldInPDFV1 = async (pdf: PDFDocument, field: FieldWithSigna
|
|||||||
radio.select(item.value);
|
radio.select(item.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.otherwise((field) => {
|
.otherwise((field) => {
|
||||||
const fieldMetaParsers = {
|
const fieldMetaParsers = {
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { RotationTypes, radiansToDegrees } from '@cantoo/pdf-lib';
|
|||||||
import fontkit from '@pdf-lib/fontkit';
|
import fontkit from '@pdf-lib/fontkit';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import 'konva/skia-backend';
|
import 'konva/skia-backend';
|
||||||
import fs from 'node:fs';
|
|
||||||
import type { Canvas } from 'skia-canvas';
|
import type { Canvas } from 'skia-canvas';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
@ -86,6 +85,7 @@ export const insertFieldInPDFV2 = async (pdf: PDFDocument, field: FieldWithSigna
|
|||||||
|
|
||||||
// Will render onto the layer.
|
// Will render onto the layer.
|
||||||
renderField({
|
renderField({
|
||||||
|
scale: 1,
|
||||||
field: {
|
field: {
|
||||||
renderId: field.id.toString(),
|
renderId: field.id.toString(),
|
||||||
...field,
|
...field,
|
||||||
@ -105,10 +105,10 @@ export const insertFieldInPDFV2 = async (pdf: PDFDocument, field: FieldWithSigna
|
|||||||
|
|
||||||
const renderedField = await canvas.toBuffer('svg');
|
const renderedField = await canvas.toBuffer('svg');
|
||||||
|
|
||||||
fs.writeFileSync(
|
// fs.writeFileSync(
|
||||||
`rendered-field-${field.envelopeId}--${field.id}.svg`,
|
// `rendered-field-${field.envelopeId}--${field.id}.svg`,
|
||||||
renderedField.toString('utf-8'),
|
// renderedField.toString('utf-8'),
|
||||||
);
|
// );
|
||||||
|
|
||||||
// Embed the SVG into the PDF
|
// Embed the SVG into the PDF
|
||||||
const svgElement = await pdf.embedSvg(renderedField.toString('utf-8'));
|
const svgElement = await pdf.embedSvg(renderedField.toString('utf-8'));
|
||||||
|
|||||||
@ -203,7 +203,6 @@ const getUpdatedFieldMeta = (field: Field, prefillField?: TFieldMetaPrefillField
|
|||||||
type: 'radio',
|
type: 'radio',
|
||||||
label: field.label,
|
label: field.label,
|
||||||
values: newValues,
|
values: newValues,
|
||||||
direction: radioMeta.direction ?? 'vertical',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return meta;
|
return meta;
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export const upsertFieldGroup = (
|
|||||||
field: FieldToRender,
|
field: FieldToRender,
|
||||||
options: RenderFieldElementOptions,
|
options: RenderFieldElementOptions,
|
||||||
): Konva.Group => {
|
): Konva.Group => {
|
||||||
const { pageWidth, pageHeight, pageLayer, editable } = options;
|
const { pageWidth, pageHeight, pageLayer, editable, scale } = options;
|
||||||
|
|
||||||
const { fieldX, fieldY, fieldWidth, fieldHeight } = calculateFieldPosition(
|
const { fieldX, fieldY, fieldWidth, fieldHeight } = calculateFieldPosition(
|
||||||
field,
|
field,
|
||||||
@ -27,6 +27,9 @@ export const upsertFieldGroup = (
|
|||||||
name: 'field-group',
|
name: 'field-group',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const maxXPosition = (pageWidth - fieldWidth) * scale;
|
||||||
|
const maxYPosition = (pageHeight - fieldHeight) * scale;
|
||||||
|
|
||||||
fieldGroup.setAttrs({
|
fieldGroup.setAttrs({
|
||||||
scaleX: 1,
|
scaleX: 1,
|
||||||
scaleY: 1,
|
scaleY: 1,
|
||||||
@ -34,8 +37,9 @@ export const upsertFieldGroup = (
|
|||||||
y: fieldY,
|
y: fieldY,
|
||||||
draggable: editable,
|
draggable: editable,
|
||||||
dragBoundFunc: (pos) => {
|
dragBoundFunc: (pos) => {
|
||||||
const newX = Math.max(0, Math.min(pageWidth - fieldWidth, pos.x));
|
const newX = Math.max(0, Math.min(maxXPosition, pos.x));
|
||||||
const newY = Math.max(0, Math.min(pageHeight - fieldHeight, pos.y));
|
const newY = Math.max(0, Math.min(maxYPosition, pos.y));
|
||||||
|
|
||||||
return { x: newX, y: newY };
|
return { x: newX, y: newY };
|
||||||
},
|
},
|
||||||
} satisfies Partial<Konva.GroupConfig>);
|
} satisfies Partial<Konva.GroupConfig>);
|
||||||
|
|||||||
@ -26,8 +26,9 @@ export type RenderFieldElementOptions = {
|
|||||||
pageLayer: Konva.Layer;
|
pageLayer: Konva.Layer;
|
||||||
pageWidth: number;
|
pageWidth: number;
|
||||||
pageHeight: number;
|
pageHeight: number;
|
||||||
mode?: 'edit' | 'sign' | 'export';
|
mode: 'edit' | 'sign' | 'export';
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
|
scale: number;
|
||||||
color?: TRecipientColor;
|
color?: TRecipientColor;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -107,6 +108,11 @@ type CalculateMultiItemPositionOptions = {
|
|||||||
*/
|
*/
|
||||||
fieldPadding: number;
|
fieldPadding: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The direction of the items.
|
||||||
|
*/
|
||||||
|
direction: 'horizontal' | 'vertical';
|
||||||
|
|
||||||
type: 'checkbox' | 'radio';
|
type: 'checkbox' | 'radio';
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -122,6 +128,7 @@ export const calculateMultiItemPosition = (options: CalculateMultiItemPositionOp
|
|||||||
itemSize,
|
itemSize,
|
||||||
spacingBetweenItemAndText,
|
spacingBetweenItemAndText,
|
||||||
fieldPadding,
|
fieldPadding,
|
||||||
|
direction,
|
||||||
type,
|
type,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
@ -130,6 +137,39 @@ export const calculateMultiItemPosition = (options: CalculateMultiItemPositionOp
|
|||||||
const innerFieldX = fieldPadding;
|
const innerFieldX = fieldPadding;
|
||||||
const innerFieldY = fieldPadding;
|
const innerFieldY = fieldPadding;
|
||||||
|
|
||||||
|
if (direction === 'horizontal') {
|
||||||
|
const itemHeight = innerFieldHeight;
|
||||||
|
const itemWidth = innerFieldWidth / itemCount;
|
||||||
|
|
||||||
|
const y = innerFieldY;
|
||||||
|
const x = itemIndex * itemWidth + innerFieldX;
|
||||||
|
|
||||||
|
let itemInputY = y + itemHeight / 2 - itemSize / 2;
|
||||||
|
let itemInputX = x;
|
||||||
|
|
||||||
|
// We need a little different logic to center the radio circle icon.
|
||||||
|
if (type === 'radio') {
|
||||||
|
itemInputX = x + itemSize / 2;
|
||||||
|
itemInputY = y + itemHeight / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textX = x + itemSize + spacingBetweenItemAndText;
|
||||||
|
const textY = y;
|
||||||
|
|
||||||
|
// Multiplied by 2 for extra padding on the right hand side of the text and the next item.
|
||||||
|
const textWidth = itemWidth - itemSize - spacingBetweenItemAndText * 2;
|
||||||
|
const textHeight = itemHeight;
|
||||||
|
|
||||||
|
return {
|
||||||
|
itemInputX,
|
||||||
|
itemInputY,
|
||||||
|
textX,
|
||||||
|
textY,
|
||||||
|
textWidth,
|
||||||
|
textHeight,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const itemHeight = innerFieldHeight / itemCount;
|
const itemHeight = innerFieldHeight / itemCount;
|
||||||
|
|
||||||
const y = itemIndex * itemHeight + innerFieldY;
|
const y = itemIndex * itemHeight + innerFieldY;
|
||||||
@ -137,6 +177,7 @@ export const calculateMultiItemPosition = (options: CalculateMultiItemPositionOp
|
|||||||
let itemInputY = y + itemHeight / 2 - itemSize / 2;
|
let itemInputY = y + itemHeight / 2 - itemSize / 2;
|
||||||
let itemInputX = innerFieldX;
|
let itemInputX = innerFieldX;
|
||||||
|
|
||||||
|
// We need a little different logic to center the radio circle icon.
|
||||||
if (type === 'radio') {
|
if (type === 'radio') {
|
||||||
itemInputX = innerFieldX + itemSize / 2;
|
itemInputX = innerFieldX + itemSize / 2;
|
||||||
itemInputY = y + itemHeight / 2;
|
itemInputY = y + itemHeight / 2;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
|
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
|
||||||
import type { TCheckboxFieldMeta } from '../../types/field-meta';
|
import type { TCheckboxFieldMeta } from '../../types/field-meta';
|
||||||
@ -21,13 +22,18 @@ export const renderCheckboxFieldElement = (
|
|||||||
|
|
||||||
const fieldGroup = upsertFieldGroup(field, options);
|
const fieldGroup = upsertFieldGroup(field, options);
|
||||||
|
|
||||||
// Clear previous children to re-render fresh
|
// Clear previous children and listeners to re-render fresh.
|
||||||
fieldGroup.removeChildren();
|
fieldGroup.removeChildren();
|
||||||
|
fieldGroup.off('transform');
|
||||||
|
|
||||||
fieldGroup.add(upsertFieldRect(field, options));
|
fieldGroup.add(upsertFieldRect(field, options));
|
||||||
|
|
||||||
|
const checkboxMeta: TCheckboxFieldMeta | null = (field.fieldMeta as TCheckboxFieldMeta) || null;
|
||||||
|
const checkboxValues = checkboxMeta?.values || [];
|
||||||
|
|
||||||
if (isFirstRender) {
|
if (isFirstRender) {
|
||||||
pageLayer.add(fieldGroup);
|
pageLayer.add(fieldGroup);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle rescaling items during transforms.
|
// Handle rescaling items during transforms.
|
||||||
fieldGroup.on('transform', () => {
|
fieldGroup.on('transform', () => {
|
||||||
@ -72,6 +78,7 @@ export const renderCheckboxFieldElement = (
|
|||||||
itemSize: checkboxSize,
|
itemSize: checkboxSize,
|
||||||
spacingBetweenItemAndText: spacingBetweenCheckboxAndText,
|
spacingBetweenItemAndText: spacingBetweenCheckboxAndText,
|
||||||
fieldPadding: checkboxFieldPadding,
|
fieldPadding: checkboxFieldPadding,
|
||||||
|
direction: checkboxMeta?.direction || 'vertical',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -111,14 +118,16 @@ export const renderCheckboxFieldElement = (
|
|||||||
|
|
||||||
pageLayer.batchDraw();
|
pageLayer.batchDraw();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const checkboxMeta: TCheckboxFieldMeta | null = (field.fieldMeta as TCheckboxFieldMeta) || null;
|
|
||||||
const checkboxValues = checkboxMeta?.values || [];
|
|
||||||
|
|
||||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
||||||
|
|
||||||
checkboxValues.forEach(({ id, value, checked }, index) => {
|
checkboxValues.forEach(({ id, value, checked }, index) => {
|
||||||
|
const isCheckboxChecked = match(mode)
|
||||||
|
.with('edit', () => checked)
|
||||||
|
.with('sign', () => value === field.customText)
|
||||||
|
.with('export', () => value === field.customText)
|
||||||
|
.exhaustive();
|
||||||
|
|
||||||
const { itemInputX, itemInputY, textX, textY, textWidth, textHeight } =
|
const { itemInputX, itemInputY, textX, textY, textWidth, textHeight } =
|
||||||
calculateMultiItemPosition({
|
calculateMultiItemPosition({
|
||||||
fieldWidth,
|
fieldWidth,
|
||||||
@ -128,6 +137,7 @@ export const renderCheckboxFieldElement = (
|
|||||||
itemSize: checkboxSize,
|
itemSize: checkboxSize,
|
||||||
spacingBetweenItemAndText: spacingBetweenCheckboxAndText,
|
spacingBetweenItemAndText: spacingBetweenCheckboxAndText,
|
||||||
fieldPadding: checkboxFieldPadding,
|
fieldPadding: checkboxFieldPadding,
|
||||||
|
direction: checkboxMeta?.direction || 'vertical',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -156,7 +166,7 @@ export const renderCheckboxFieldElement = (
|
|||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
stroke: '#111827',
|
stroke: '#111827',
|
||||||
points: [3, 8, 7, 12, 13, 4],
|
points: [3, 8, 7, 12, 13, 4],
|
||||||
visible: checked,
|
visible: isCheckboxChecked,
|
||||||
});
|
});
|
||||||
|
|
||||||
const text = new Konva.Text({
|
const text = new Konva.Text({
|
||||||
|
|||||||
@ -47,6 +47,7 @@ type RenderFieldOptions = {
|
|||||||
*/
|
*/
|
||||||
mode: 'edit' | 'sign' | 'export';
|
mode: 'edit' | 'sign' | 'export';
|
||||||
|
|
||||||
|
scale: number;
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -56,6 +57,7 @@ export const renderField = ({
|
|||||||
pageWidth,
|
pageWidth,
|
||||||
pageHeight,
|
pageHeight,
|
||||||
mode,
|
mode,
|
||||||
|
scale,
|
||||||
editable,
|
editable,
|
||||||
color,
|
color,
|
||||||
}: RenderFieldOptions) => {
|
}: RenderFieldOptions) => {
|
||||||
@ -66,6 +68,7 @@ export const renderField = ({
|
|||||||
mode,
|
mode,
|
||||||
color,
|
color,
|
||||||
editable,
|
editable,
|
||||||
|
scale,
|
||||||
};
|
};
|
||||||
|
|
||||||
return match(field.type)
|
return match(field.type)
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
|
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
|
||||||
import type { TRadioFieldMeta } from '../../types/field-meta';
|
import type { TRadioFieldMeta } from '../../types/field-meta';
|
||||||
@ -26,8 +27,14 @@ export const renderRadioFieldElement = (
|
|||||||
|
|
||||||
fieldGroup.add(upsertFieldRect(field, options));
|
fieldGroup.add(upsertFieldRect(field, options));
|
||||||
|
|
||||||
|
const radioMeta: TRadioFieldMeta | null = (field.fieldMeta as TRadioFieldMeta) || null;
|
||||||
|
const radioValues = radioMeta?.values || [];
|
||||||
|
|
||||||
if (isFirstRender) {
|
if (isFirstRender) {
|
||||||
pageLayer.add(fieldGroup);
|
pageLayer.add(fieldGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldGroup.off('transform');
|
||||||
|
|
||||||
// Handle rescaling items during transforms.
|
// Handle rescaling items during transforms.
|
||||||
fieldGroup.on('transform', () => {
|
fieldGroup.on('transform', () => {
|
||||||
@ -66,6 +73,7 @@ export const renderRadioFieldElement = (
|
|||||||
spacingBetweenItemAndText: spacingBetweenRadioAndText,
|
spacingBetweenItemAndText: spacingBetweenRadioAndText,
|
||||||
fieldPadding: radioFieldPadding,
|
fieldPadding: radioFieldPadding,
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
|
direction: radioMeta?.direction || 'vertical',
|
||||||
});
|
});
|
||||||
|
|
||||||
circleElement.setAttrs({
|
circleElement.setAttrs({
|
||||||
@ -102,14 +110,16 @@ export const renderRadioFieldElement = (
|
|||||||
|
|
||||||
pageLayer.batchDraw();
|
pageLayer.batchDraw();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const radioMeta: TRadioFieldMeta | null = (field.fieldMeta as TRadioFieldMeta) || null;
|
|
||||||
const radioValues = radioMeta?.values || [];
|
|
||||||
|
|
||||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
||||||
|
|
||||||
radioValues.forEach(({ value, checked }, index) => {
|
radioValues.forEach(({ value, checked }, index) => {
|
||||||
|
const isRadioValueChecked = match(mode)
|
||||||
|
.with('edit', () => checked)
|
||||||
|
.with('sign', () => value === field.customText)
|
||||||
|
.with('export', () => value === field.customText)
|
||||||
|
.exhaustive();
|
||||||
|
|
||||||
const { itemInputX, itemInputY, textX, textY, textWidth, textHeight } =
|
const { itemInputX, itemInputY, textX, textY, textWidth, textHeight } =
|
||||||
calculateMultiItemPosition({
|
calculateMultiItemPosition({
|
||||||
fieldWidth,
|
fieldWidth,
|
||||||
@ -120,6 +130,7 @@ export const renderRadioFieldElement = (
|
|||||||
spacingBetweenItemAndText: spacingBetweenRadioAndText,
|
spacingBetweenItemAndText: spacingBetweenRadioAndText,
|
||||||
fieldPadding: radioFieldPadding,
|
fieldPadding: radioFieldPadding,
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
|
direction: radioMeta?.direction || 'vertical',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Circle which represents the radio button.
|
// Circle which represents the radio button.
|
||||||
@ -144,9 +155,7 @@ export const renderRadioFieldElement = (
|
|||||||
y: itemInputY,
|
y: itemInputY,
|
||||||
radius: radioSize / 4,
|
radius: radioSize / 4,
|
||||||
fill: '#111827',
|
fill: '#111827',
|
||||||
// Todo: Envelopes
|
visible: isRadioValueChecked,
|
||||||
visible: value === field.customText,
|
|
||||||
// visible: checked,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const text = new Konva.Text({
|
const text = new Konva.Text({
|
||||||
|
|||||||
@ -96,17 +96,21 @@ export const renderSignatureFieldElement = (
|
|||||||
|
|
||||||
const fieldGroup = upsertFieldGroup(field, options);
|
const fieldGroup = upsertFieldGroup(field, options);
|
||||||
|
|
||||||
// ABOVE IS GENERIC, EXTRACT IT.
|
// Clear previous children and listeners to re-render fresh.
|
||||||
|
fieldGroup.removeChildren();
|
||||||
|
fieldGroup.off('transform');
|
||||||
|
|
||||||
|
// Assign elements to group and any listeners that should only be run on initialization.
|
||||||
|
if (isFirstRender) {
|
||||||
|
pageLayer.add(fieldGroup);
|
||||||
|
}
|
||||||
|
|
||||||
// Render the field background and text.
|
// Render the field background and text.
|
||||||
const fieldRect = upsertFieldRect(field, options);
|
const fieldRect = upsertFieldRect(field, options);
|
||||||
const fieldText = upsertFieldText(field, options);
|
const fieldText = upsertFieldText(field, options);
|
||||||
|
|
||||||
// Assign elements to group and any listeners that should only be run on initialization.
|
|
||||||
if (isFirstRender) {
|
|
||||||
fieldGroup.add(fieldRect);
|
fieldGroup.add(fieldRect);
|
||||||
fieldGroup.add(fieldText);
|
fieldGroup.add(fieldText);
|
||||||
pageLayer.add(fieldGroup);
|
|
||||||
|
|
||||||
// This is to keep the text inside the field at the same size
|
// This is to keep the text inside the field at the same size
|
||||||
// when the field is resized. Without this the text would be stretched.
|
// when the field is resized. Without this the text would be stretched.
|
||||||
@ -166,7 +170,6 @@ export const renderSignatureFieldElement = (
|
|||||||
// fieldGroup.draw();
|
// fieldGroup.draw();
|
||||||
fieldGroup.getLayer()?.batchDraw();
|
fieldGroup.getLayer()?.batchDraw();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Handle export mode.
|
// Handle export mode.
|
||||||
if (mode === 'export') {
|
if (mode === 'export') {
|
||||||
|
|||||||
@ -121,17 +121,21 @@ export const renderTextFieldElement = (
|
|||||||
|
|
||||||
const fieldGroup = upsertFieldGroup(field, options);
|
const fieldGroup = upsertFieldGroup(field, options);
|
||||||
|
|
||||||
// ABOVE IS GENERIC, EXTRACT IT.
|
// Clear previous children and listeners to re-render fresh.
|
||||||
|
fieldGroup.removeChildren();
|
||||||
|
fieldGroup.off('transform');
|
||||||
|
|
||||||
|
// Assign elements to group and any listeners that should only be run on initialization.
|
||||||
|
if (isFirstRender) {
|
||||||
|
pageLayer.add(fieldGroup);
|
||||||
|
}
|
||||||
|
|
||||||
// Render the field background and text.
|
// Render the field background and text.
|
||||||
const fieldRect = upsertFieldRect(field, options);
|
const fieldRect = upsertFieldRect(field, options);
|
||||||
const fieldText = upsertFieldText(field, options);
|
const fieldText = upsertFieldText(field, options);
|
||||||
|
|
||||||
// Assign elements to group and any listeners that should only be run on initialization.
|
|
||||||
if (isFirstRender) {
|
|
||||||
fieldGroup.add(fieldRect);
|
fieldGroup.add(fieldRect);
|
||||||
fieldGroup.add(fieldText);
|
fieldGroup.add(fieldText);
|
||||||
pageLayer.add(fieldGroup);
|
|
||||||
|
|
||||||
// This is to keep the text inside the field at the same size
|
// This is to keep the text inside the field at the same size
|
||||||
// when the field is resized. Without this the text would be stretched.
|
// when the field is resized. Without this the text would be stretched.
|
||||||
@ -191,7 +195,6 @@ export const renderTextFieldElement = (
|
|||||||
// fieldGroup.draw();
|
// fieldGroup.draw();
|
||||||
fieldGroup.getLayer()?.batchDraw();
|
fieldGroup.getLayer()?.batchDraw();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Handle export mode.
|
// Handle export mode.
|
||||||
if (mode === 'export') {
|
if (mode === 'export') {
|
||||||
|
|||||||
@ -91,11 +91,11 @@ export const PdfViewerKonva = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={$el} className={cn('w-[800px] overflow-hidden', className)} {...props}>
|
<div ref={$el} className={cn('w-full max-w-[800px]', className)} {...props}>
|
||||||
{envelopeItemFile && Konva ? (
|
{envelopeItemFile && Konva ? (
|
||||||
<PDFDocument
|
<PDFDocument
|
||||||
file={envelopeItemFile}
|
file={envelopeItemFile}
|
||||||
className={cn('w-full overflow-hidden rounded', {
|
className={cn('w-full rounded', {
|
||||||
'h-[80vh] max-h-[60rem]': numPages === 0,
|
'h-[80vh] max-h-[60rem]': numPages === 0,
|
||||||
})}
|
})}
|
||||||
onLoadSuccess={(d) => onDocumentLoaded(d)}
|
onLoadSuccess={(d) => onDocumentLoaded(d)}
|
||||||
@ -138,7 +138,7 @@ export const PdfViewerKonva = ({
|
|||||||
.fill(null)
|
.fill(null)
|
||||||
.map((_, i) => (
|
.map((_, i) => (
|
||||||
<div key={i} className="last:-mb-2">
|
<div key={i} className="last:-mb-2">
|
||||||
<div className="border-border overflow-hidden rounded border will-change-transform">
|
<div className="border-border rounded border will-change-transform">
|
||||||
<PDFPage
|
<PDFPage
|
||||||
pageNumber={i + 1}
|
pageNumber={i + 1}
|
||||||
width={width}
|
width={width}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export type RecipientColorStyles = {
|
|||||||
base: string;
|
base: string;
|
||||||
baseRing: string;
|
baseRing: string;
|
||||||
baseRingHover: string;
|
baseRingHover: string;
|
||||||
|
fieldButton: string;
|
||||||
fieldItem: string;
|
fieldItem: string;
|
||||||
fieldItemInitials: string;
|
fieldItemInitials: string;
|
||||||
comboxBoxTrigger: string;
|
comboxBoxTrigger: string;
|
||||||
@ -23,6 +24,7 @@ export const RECIPIENT_COLOR_STYLES = {
|
|||||||
base: 'ring-neutral-400',
|
base: 'ring-neutral-400',
|
||||||
baseRing: 'rgba(176, 176, 176, 1)',
|
baseRing: 'rgba(176, 176, 176, 1)',
|
||||||
baseRingHover: 'rgba(176, 176, 176, 1)',
|
baseRingHover: 'rgba(176, 176, 176, 1)',
|
||||||
|
fieldButton: 'border-neutral-400 hover:border-neutral-400',
|
||||||
fieldItem: 'group/field-item rounded-[2px]',
|
fieldItem: 'group/field-item rounded-[2px]',
|
||||||
fieldItemInitials: '',
|
fieldItemInitials: '',
|
||||||
comboxBoxTrigger:
|
comboxBoxTrigger:
|
||||||
@ -34,6 +36,7 @@ export const RECIPIENT_COLOR_STYLES = {
|
|||||||
base: 'ring-recipient-green hover:bg-recipient-green/30',
|
base: 'ring-recipient-green hover:bg-recipient-green/30',
|
||||||
baseRing: 'rgba(122, 195, 85, 1)',
|
baseRing: 'rgba(122, 195, 85, 1)',
|
||||||
baseRingHover: 'rgba(122, 195, 85, 0.3)',
|
baseRingHover: 'rgba(122, 195, 85, 0.3)',
|
||||||
|
fieldButton: 'hover:border-recipient-green hover:bg-recipient-green/30 ',
|
||||||
fieldItem: 'group/field-item rounded-[2px]',
|
fieldItem: 'group/field-item rounded-[2px]',
|
||||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-green',
|
fieldItemInitials: 'group-hover/field-item:bg-recipient-green',
|
||||||
comboxBoxTrigger:
|
comboxBoxTrigger:
|
||||||
@ -45,6 +48,7 @@ export const RECIPIENT_COLOR_STYLES = {
|
|||||||
base: 'ring-recipient-blue hover:bg-recipient-blue/30',
|
base: 'ring-recipient-blue hover:bg-recipient-blue/30',
|
||||||
baseRing: 'rgba(56, 123, 199, 1)',
|
baseRing: 'rgba(56, 123, 199, 1)',
|
||||||
baseRingHover: 'rgba(56, 123, 199, 0.3)',
|
baseRingHover: 'rgba(56, 123, 199, 0.3)',
|
||||||
|
fieldButton: 'hover:border-recipient-blue hover:bg-recipient-blue/30',
|
||||||
fieldItem: 'group/field-item rounded-[2px]',
|
fieldItem: 'group/field-item rounded-[2px]',
|
||||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-blue',
|
fieldItemInitials: 'group-hover/field-item:bg-recipient-blue',
|
||||||
comboxBoxTrigger:
|
comboxBoxTrigger:
|
||||||
@ -56,6 +60,7 @@ export const RECIPIENT_COLOR_STYLES = {
|
|||||||
base: 'ring-recipient-purple hover:bg-recipient-purple/30',
|
base: 'ring-recipient-purple hover:bg-recipient-purple/30',
|
||||||
baseRing: 'rgba(151, 71, 255, 1)',
|
baseRing: 'rgba(151, 71, 255, 1)',
|
||||||
baseRingHover: 'rgba(151, 71, 255, 0.3)',
|
baseRingHover: 'rgba(151, 71, 255, 0.3)',
|
||||||
|
fieldButton: 'hover:border-recipient-purple hover:bg-recipient-purple/30',
|
||||||
fieldItem: 'group/field-item rounded-[2px]',
|
fieldItem: 'group/field-item rounded-[2px]',
|
||||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-purple',
|
fieldItemInitials: 'group-hover/field-item:bg-recipient-purple',
|
||||||
comboxBoxTrigger:
|
comboxBoxTrigger:
|
||||||
@ -67,6 +72,7 @@ export const RECIPIENT_COLOR_STYLES = {
|
|||||||
base: 'ring-recipient-orange hover:bg-recipient-orange/30',
|
base: 'ring-recipient-orange hover:bg-recipient-orange/30',
|
||||||
baseRing: 'rgba(246, 159, 30, 1)',
|
baseRing: 'rgba(246, 159, 30, 1)',
|
||||||
baseRingHover: 'rgba(246, 159, 30, 0.3)',
|
baseRingHover: 'rgba(246, 159, 30, 0.3)',
|
||||||
|
fieldButton: 'hover:border-recipient-orange hover:bg-recipient-orange/30',
|
||||||
fieldItem: 'group/field-item rounded-[2px]',
|
fieldItem: 'group/field-item rounded-[2px]',
|
||||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-orange',
|
fieldItemInitials: 'group-hover/field-item:bg-recipient-orange',
|
||||||
comboxBoxTrigger:
|
comboxBoxTrigger:
|
||||||
@ -78,6 +84,7 @@ export const RECIPIENT_COLOR_STYLES = {
|
|||||||
base: 'ring-recipient-yellow hover:bg-recipient-yellow/30',
|
base: 'ring-recipient-yellow hover:bg-recipient-yellow/30',
|
||||||
baseRing: 'rgba(219, 186, 0, 1)',
|
baseRing: 'rgba(219, 186, 0, 1)',
|
||||||
baseRingHover: 'rgba(219, 186, 0, 0.3)',
|
baseRingHover: 'rgba(219, 186, 0, 0.3)',
|
||||||
|
fieldButton: 'hover:border-recipient-yellow hover:bg-recipient-yellow/30',
|
||||||
fieldItem: 'group/field-item rounded-[2px]',
|
fieldItem: 'group/field-item rounded-[2px]',
|
||||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-yellow',
|
fieldItemInitials: 'group-hover/field-item:bg-recipient-yellow',
|
||||||
comboxBoxTrigger:
|
comboxBoxTrigger:
|
||||||
@ -89,6 +96,7 @@ export const RECIPIENT_COLOR_STYLES = {
|
|||||||
base: 'ring-recipient-pink hover:bg-recipient-pink/30',
|
base: 'ring-recipient-pink hover:bg-recipient-pink/30',
|
||||||
baseRing: 'rgba(217, 74, 186, 1)',
|
baseRing: 'rgba(217, 74, 186, 1)',
|
||||||
baseRingHover: 'rgba(217, 74, 186, 0.3)',
|
baseRingHover: 'rgba(217, 74, 186, 0.3)',
|
||||||
|
fieldButton: 'hover:border-recipient-pink hover:bg-recipient-pink/30',
|
||||||
fieldItem: 'group/field-item rounded-[2px]',
|
fieldItem: 'group/field-item rounded-[2px]',
|
||||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-pink',
|
fieldItemInitials: 'group-hover/field-item:bg-recipient-pink',
|
||||||
comboxBoxTrigger:
|
comboxBoxTrigger:
|
||||||
|
|||||||
@ -108,15 +108,8 @@ export const FieldContent = ({ field, documentMeta }: FieldIconProps) => {
|
|||||||
field.fieldMeta.values.length > 0
|
field.fieldMeta.values.length > 0
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div className="py-0.5">
|
<div className="flex flex-col gap-y-2 py-0.5">
|
||||||
<RadioGroup
|
<RadioGroup className="gap-y-1">
|
||||||
className={cn(
|
|
||||||
'gap-1',
|
|
||||||
field.fieldMeta.direction === 'horizontal'
|
|
||||||
? 'flex flex-row flex-wrap'
|
|
||||||
: 'flex flex-col gap-y-1',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{field.fieldMeta.values.map((item, index) => (
|
{field.fieldMeta.values.map((item, index) => (
|
||||||
<div key={index} className="flex items-center">
|
<div key={index} className="flex items-center">
|
||||||
<RadioGroupItem
|
<RadioGroupItem
|
||||||
|
|||||||
@ -122,7 +122,6 @@ const getDefaultState = (fieldType: FieldType): FieldMeta => {
|
|||||||
values: [],
|
values: [],
|
||||||
required: false,
|
required: false,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
direction: 'vertical',
|
|
||||||
};
|
};
|
||||||
case FieldType.CHECKBOX:
|
case FieldType.CHECKBOX:
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -11,13 +11,6 @@ import { Button } from '@documenso/ui/primitives/button';
|
|||||||
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@documenso/ui/primitives/select';
|
|
||||||
import { Switch } from '@documenso/ui/primitives/switch';
|
import { Switch } from '@documenso/ui/primitives/switch';
|
||||||
|
|
||||||
export type RadioFieldAdvancedSettingsProps = {
|
export type RadioFieldAdvancedSettingsProps = {
|
||||||
@ -42,9 +35,6 @@ export const RadioFieldAdvancedSettings = ({
|
|||||||
);
|
);
|
||||||
const [readOnly, setReadOnly] = useState(fieldState.readOnly ?? false);
|
const [readOnly, setReadOnly] = useState(fieldState.readOnly ?? false);
|
||||||
const [required, setRequired] = useState(fieldState.required ?? false);
|
const [required, setRequired] = useState(fieldState.required ?? false);
|
||||||
const [direction, setDirection] = useState<'vertical' | 'horizontal'>(
|
|
||||||
fieldState.direction ?? 'vertical',
|
|
||||||
);
|
|
||||||
|
|
||||||
const addValue = () => {
|
const addValue = () => {
|
||||||
const newId = values.length > 0 ? Math.max(...values.map((val) => val.id)) + 1 : 1;
|
const newId = values.length > 0 ? Math.max(...values.map((val) => val.id)) + 1 : 1;
|
||||||
@ -79,19 +69,10 @@ export const RadioFieldAdvancedSettings = ({
|
|||||||
const handleToggleChange = (field: keyof RadioFieldMeta, value: string | boolean) => {
|
const handleToggleChange = (field: keyof RadioFieldMeta, value: string | boolean) => {
|
||||||
const readOnly = field === 'readOnly' ? Boolean(value) : Boolean(fieldState.readOnly);
|
const readOnly = field === 'readOnly' ? Boolean(value) : Boolean(fieldState.readOnly);
|
||||||
const required = field === 'required' ? Boolean(value) : Boolean(fieldState.required);
|
const required = field === 'required' ? Boolean(value) : Boolean(fieldState.required);
|
||||||
const currentDirection =
|
|
||||||
field === 'direction' && String(value) === 'horizontal' ? 'horizontal' : 'vertical';
|
|
||||||
setReadOnly(readOnly);
|
setReadOnly(readOnly);
|
||||||
setRequired(required);
|
setRequired(required);
|
||||||
setDirection(currentDirection);
|
|
||||||
|
|
||||||
const errors = validateRadioField(String(value), {
|
const errors = validateRadioField(String(value), { readOnly, required, values, type: 'radio' });
|
||||||
readOnly,
|
|
||||||
required,
|
|
||||||
values,
|
|
||||||
type: 'radio',
|
|
||||||
direction: currentDirection,
|
|
||||||
});
|
|
||||||
handleErrors(errors);
|
handleErrors(errors);
|
||||||
|
|
||||||
handleFieldChange(field, value);
|
handleFieldChange(field, value);
|
||||||
@ -116,13 +97,7 @@ export const RadioFieldAdvancedSettings = ({
|
|||||||
}, [fieldState.values]);
|
}, [fieldState.values]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const errors = validateRadioField(undefined, {
|
const errors = validateRadioField(undefined, { readOnly, required, values, type: 'radio' });
|
||||||
readOnly,
|
|
||||||
required,
|
|
||||||
values,
|
|
||||||
type: 'radio',
|
|
||||||
direction,
|
|
||||||
});
|
|
||||||
handleErrors(errors);
|
handleErrors(errors);
|
||||||
}, [values]);
|
}, [values]);
|
||||||
|
|
||||||
@ -141,27 +116,6 @@ export const RadioFieldAdvancedSettings = ({
|
|||||||
onChange={(e) => handleFieldChange('label', e.target.value)}
|
onChange={(e) => handleFieldChange('label', e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<Label>
|
|
||||||
<Trans>Direction</Trans>
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={fieldState.direction ?? 'vertical'}
|
|
||||||
onValueChange={(val) => handleToggleChange('direction', val)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="text-muted-foreground bg-background mt-2 w-full">
|
|
||||||
<SelectValue placeholder={_(msg`Select direction`)} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent position="popper">
|
|
||||||
<SelectItem value="vertical">
|
|
||||||
<Trans>Vertical</Trans>
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="horizontal">
|
|
||||||
<Trans>Horizontal</Trans>
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<Switch
|
<Switch
|
||||||
className="bg-background"
|
className="bg-background"
|
||||||
|
|||||||
Reference in New Issue
Block a user