mirror of
https://github.com/documenso/documenso.git
synced 2025-11-21 12:11:29 +10:00
fix: merge conflicts
This commit is contained in:
@ -35,6 +35,7 @@ import {
|
||||
ZFieldMetaSchema,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { ADVANCED_FIELD_TYPES_WITH_OPTIONAL_SETTING } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
import { validateFieldsUninserted } from '@documenso/lib/utils/fields';
|
||||
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
|
||||
import {
|
||||
@ -131,6 +132,7 @@ export const AddFieldsFormPartial = ({
|
||||
currentStep === 1 && typeof documentFlow.onBackStep === 'function' && canGoBack;
|
||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
||||
const [currentField, setCurrentField] = useState<FieldFormType>();
|
||||
const [activeFieldId, setActiveFieldId] = useState<string | null>(null);
|
||||
|
||||
const form = useForm<TAddFieldsFormSchema>({
|
||||
defaultValues: {
|
||||
@ -354,6 +356,13 @@ export const AddFieldsFormPartial = ({
|
||||
|
||||
append(field);
|
||||
|
||||
// Only open fields with significant amount of settings (instead of just a font setting) to
|
||||
// reduce friction when adding fields.
|
||||
if (ADVANCED_FIELD_TYPES_WITH_OPTIONAL_SETTING.includes(selectedField)) {
|
||||
setCurrentField(field);
|
||||
setShowAdvancedSettings(true);
|
||||
}
|
||||
|
||||
setIsFieldWithinBounds(false);
|
||||
setSelectedField(null);
|
||||
},
|
||||
@ -501,7 +510,15 @@ export const AddFieldsFormPartial = ({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedSigner(recipients.find((r) => r.sendStatus !== SendStatus.SENT) ?? recipients[0]);
|
||||
const recipientsByRoleToDisplay = recipients.filter(
|
||||
(recipient) =>
|
||||
recipient.role !== RecipientRole.CC && recipient.role !== RecipientRole.ASSISTANT,
|
||||
);
|
||||
|
||||
setSelectedSigner(
|
||||
recipientsByRoleToDisplay.find((r) => r.sendStatus !== SendStatus.SENT) ??
|
||||
recipientsByRoleToDisplay[0],
|
||||
);
|
||||
}, [recipients]);
|
||||
|
||||
const recipientsByRole = useMemo(() => {
|
||||
@ -510,6 +527,7 @@ export const AddFieldsFormPartial = ({
|
||||
VIEWER: [],
|
||||
SIGNER: [],
|
||||
APPROVER: [],
|
||||
ASSISTANT: [],
|
||||
};
|
||||
|
||||
recipients.forEach((recipient) => {
|
||||
@ -522,7 +540,12 @@ export const AddFieldsFormPartial = ({
|
||||
const recipientsByRoleToDisplay = useMemo(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][])
|
||||
.filter(([role]) => role !== RecipientRole.CC && role !== RecipientRole.VIEWER)
|
||||
.filter(
|
||||
([role]) =>
|
||||
role !== RecipientRole.CC &&
|
||||
role !== RecipientRole.VIEWER &&
|
||||
role !== RecipientRole.ASSISTANT,
|
||||
)
|
||||
.map(
|
||||
([role, roleRecipients]) =>
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
@ -537,12 +560,6 @@ export const AddFieldsFormPartial = ({
|
||||
);
|
||||
}, [recipientsByRole]);
|
||||
|
||||
const isTypedSignatureEnabled = form.watch('typedSignatureEnabled');
|
||||
|
||||
const handleTypedSignatureChange = (value: boolean) => {
|
||||
form.setValue('typedSignatureEnabled', value, { shouldDirty: true });
|
||||
};
|
||||
|
||||
const hasSameOwnerAsRecipient =
|
||||
recipientsByRole.SIGNER.length === 1 &&
|
||||
recipientsByRole.SIGNER[0].email === session?.user?.email;
|
||||
@ -658,6 +675,9 @@ export const AddFieldsFormPartial = ({
|
||||
}}
|
||||
hideRecipients={hideRecipients}
|
||||
hasErrors={!!hasFieldError}
|
||||
active={activeFieldId === field.formId}
|
||||
onFieldActivate={() => setActiveFieldId(field.formId)}
|
||||
onFieldDeactivate={() => setActiveFieldId(null)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@ -681,9 +701,7 @@ export const AddFieldsFormPartial = ({
|
||||
)}
|
||||
|
||||
{!selectedSigner?.email && (
|
||||
<span className="gradie flex-1 truncate text-left">
|
||||
{selectedSigner?.email}
|
||||
</span>
|
||||
<span className="flex-1 truncate text-left">{selectedSigner?.email}</span>
|
||||
)}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4" />
|
||||
@ -794,7 +812,6 @@ export const AddFieldsFormPartial = ({
|
||||
<Checkbox
|
||||
{...field}
|
||||
id="typedSignatureEnabled"
|
||||
checkClassName="text-white"
|
||||
checked={value}
|
||||
onCheckedChange={(checked) => field.onChange(checked)}
|
||||
disabled={form.formState.isSubmitting}
|
||||
@ -1088,8 +1105,8 @@ export const AddFieldsFormPartial = ({
|
||||
{emptyCheckboxFields.length > 0
|
||||
? 'Checkbox'
|
||||
: emptyRadioFields.length > 0
|
||||
? 'Radio'
|
||||
: 'Select'}{' '}
|
||||
? 'Radio'
|
||||
: 'Select'}{' '}
|
||||
field.
|
||||
</Trans>
|
||||
</li>
|
||||
|
||||
@ -6,14 +6,15 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import type { TDocument } from '@documenso/lib/types/document';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import type { TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, type Field, type Recipient, SendStatus } from '@documenso/prisma/client';
|
||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||
import {
|
||||
DocumentGlobalAuthAccessSelect,
|
||||
DocumentGlobalAuthAccessTooltip,
|
||||
@ -64,7 +65,7 @@ export type AddSettingsFormProps = {
|
||||
fields: Field[];
|
||||
isDocumentEnterprise: boolean;
|
||||
isDocumentPdfLoaded: boolean;
|
||||
document: DocumentWithData;
|
||||
document: TDocument;
|
||||
currentTeamMemberRole?: TeamMemberRole;
|
||||
onSubmit: (_data: TAddSettingsFormSchema) => void;
|
||||
};
|
||||
@ -110,6 +111,16 @@ export const AddSettingsFormPartial = ({
|
||||
(recipient) => recipient.sendStatus === SendStatus.SENT,
|
||||
);
|
||||
|
||||
const canUpdateVisibility = match(currentTeamMemberRole)
|
||||
.with(TeamMemberRole.ADMIN, () => true)
|
||||
.with(
|
||||
TeamMemberRole.MANAGER,
|
||||
() =>
|
||||
document.visibility === DocumentVisibility.EVERYONE ||
|
||||
document.visibility === DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
)
|
||||
.otherwise(() => false);
|
||||
|
||||
// We almost always want to set the timezone to the user's local timezone to avoid confusion
|
||||
// when the document is signed.
|
||||
useEffect(() => {
|
||||
@ -237,7 +248,8 @@ export const AddSettingsFormPartial = ({
|
||||
|
||||
<FormControl>
|
||||
<DocumentVisibilitySelect
|
||||
currentMemberRole={currentTeamMemberRole}
|
||||
canUpdateVisibility={canUpdateVisibility}
|
||||
currentTeamMemberRole={currentTeamMemberRole}
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
@ -273,7 +285,7 @@ export const AddSettingsFormPartial = ({
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-2 text-sm leading-relaxed">
|
||||
<div className="flex flex-col space-y-6 ">
|
||||
<div className="flex flex-col space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="externalId"
|
||||
|
||||
@ -9,6 +9,10 @@ import {
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
||||
import { DocumentVisibility } from '@documenso/prisma/client';
|
||||
import {
|
||||
ZDocumentMetaDateFormatSchema,
|
||||
ZDocumentMetaTimezoneSchema,
|
||||
} from '@documenso/trpc/server/document-router/schema';
|
||||
|
||||
export const ZMapNegativeOneToUndefinedSchema = z
|
||||
.string()
|
||||
@ -32,8 +36,8 @@ export const ZAddSettingsFormSchema = z.object({
|
||||
ZDocumentActionAuthTypesSchema.optional(),
|
||||
),
|
||||
meta: z.object({
|
||||
timezone: z.string().optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
||||
dateFormat: z.string().optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
||||
timezone: ZDocumentMetaTimezoneSchema.optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
||||
dateFormat: ZDocumentMetaDateFormatSchema.optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
||||
redirectUrl: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@ -1,435 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { DateTime } from 'luxon';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||
import type { Field } from '@documenso/prisma/client';
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
|
||||
|
||||
import { FieldToolTip } from '../../components/field/field-tooltip';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Card, CardContent } from '../card';
|
||||
import { ElementVisible } from '../element-visible';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
|
||||
import { Input } from '../input';
|
||||
import { SignaturePad } from '../signature-pad';
|
||||
import { useStep } from '../stepper';
|
||||
import type { TAddSignatureFormSchema } from './add-signature.types';
|
||||
import { ZAddSignatureFormSchema } from './add-signature.types';
|
||||
import {
|
||||
DocumentFlowFormContainerActions,
|
||||
DocumentFlowFormContainerContent,
|
||||
DocumentFlowFormContainerFooter,
|
||||
DocumentFlowFormContainerHeader,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
import {
|
||||
SinglePlayerModeCustomTextField,
|
||||
SinglePlayerModeSignatureField,
|
||||
} from './single-player-mode-fields';
|
||||
|
||||
export type AddSignatureFormProps = {
|
||||
defaultValues?: TAddSignatureFormSchema;
|
||||
documentFlow: DocumentFlowStep;
|
||||
fields: FieldWithSignature[];
|
||||
|
||||
onSubmit: (_data: TAddSignatureFormSchema) => Promise<void> | void;
|
||||
requireName?: boolean;
|
||||
requireCustomText?: boolean;
|
||||
requireSignature?: boolean;
|
||||
};
|
||||
|
||||
export const AddSignatureFormPartial = ({
|
||||
defaultValues,
|
||||
documentFlow,
|
||||
fields,
|
||||
|
||||
onSubmit,
|
||||
requireName = false,
|
||||
requireCustomText = false,
|
||||
requireSignature = true,
|
||||
}: AddSignatureFormProps) => {
|
||||
const { currentStep, totalSteps } = useStep();
|
||||
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||
|
||||
// Refined schema which takes into account whether to allow an empty name or signature.
|
||||
const refinedSchema = ZAddSignatureFormSchema.superRefine((val, ctx) => {
|
||||
if (requireName && val.name.length === 0) {
|
||||
ctx.addIssue({
|
||||
path: ['name'],
|
||||
code: 'custom',
|
||||
message: 'Name is required',
|
||||
});
|
||||
}
|
||||
|
||||
if (requireCustomText && val.customText.length === 0) {
|
||||
ctx.addIssue({
|
||||
path: ['customText'],
|
||||
code: 'custom',
|
||||
message: 'Text is required',
|
||||
});
|
||||
}
|
||||
|
||||
if (requireSignature && val.signature.length === 0) {
|
||||
ctx.addIssue({
|
||||
path: ['signature'],
|
||||
code: 'custom',
|
||||
message: 'Signature is required',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const form = useForm<TAddSignatureFormSchema>({
|
||||
resolver: zodResolver(refinedSchema),
|
||||
defaultValues: defaultValues ?? {
|
||||
name: '',
|
||||
email: '',
|
||||
signature: '',
|
||||
customText: '',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* A local copy of the provided fields to modify.
|
||||
*/
|
||||
const [localFields, setLocalFields] = useState<Field[]>(JSON.parse(JSON.stringify(fields)));
|
||||
|
||||
const uninsertedFields = useMemo(() => {
|
||||
const fields = localFields.filter((field) => !field.inserted);
|
||||
|
||||
return sortFieldsByPosition(fields);
|
||||
}, [localFields]);
|
||||
|
||||
const onValidateFields = async (values: TAddSignatureFormSchema) => {
|
||||
setValidateUninsertedFields(true);
|
||||
const isFieldsValid = validateFieldsInserted(localFields);
|
||||
|
||||
if (!isFieldsValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
await onSubmit(values);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates whether the corresponding form for a given field type is valid.
|
||||
*
|
||||
* @returns `true` if the form associated with the provided field is valid, `false` otherwise.
|
||||
*/
|
||||
const validateFieldForm = async (fieldType: Field['type']): Promise<boolean> => {
|
||||
if (fieldType === FieldType.SIGNATURE) {
|
||||
await form.trigger('signature');
|
||||
return !form.formState.errors.signature;
|
||||
}
|
||||
|
||||
if (fieldType === FieldType.NAME) {
|
||||
await form.trigger('name');
|
||||
return !form.formState.errors.name;
|
||||
}
|
||||
|
||||
if (fieldType === FieldType.EMAIL) {
|
||||
await form.trigger('email');
|
||||
return !form.formState.errors.email;
|
||||
}
|
||||
|
||||
if (fieldType === FieldType.TEXT) {
|
||||
await form.trigger('customText');
|
||||
return !form.formState.errors.customText;
|
||||
}
|
||||
|
||||
if (fieldType === FieldType.NUMBER) {
|
||||
await form.trigger('number');
|
||||
return !form.formState.errors.number;
|
||||
}
|
||||
|
||||
if (fieldType === FieldType.RADIO) {
|
||||
await form.trigger('radio');
|
||||
return !form.formState.errors.radio;
|
||||
}
|
||||
|
||||
if (fieldType === FieldType.CHECKBOX) {
|
||||
await form.trigger('checkbox');
|
||||
return !form.formState.errors.checkbox;
|
||||
}
|
||||
|
||||
if (fieldType === FieldType.DROPDOWN) {
|
||||
await form.trigger('dropdown');
|
||||
return !form.formState.errors.dropdown;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Insert the corresponding form value into a given field.
|
||||
*/
|
||||
const insertFormValueIntoField = (field: Field) => {
|
||||
return match(field.type)
|
||||
.with(FieldType.DATE, () => ({
|
||||
...field,
|
||||
customText: DateTime.now().toFormat(DEFAULT_DOCUMENT_DATE_FORMAT),
|
||||
inserted: true,
|
||||
}))
|
||||
.with(FieldType.EMAIL, () => ({
|
||||
...field,
|
||||
customText: form.getValues('email'),
|
||||
inserted: true,
|
||||
}))
|
||||
.with(FieldType.NAME, () => ({
|
||||
...field,
|
||||
customText: form.getValues('name'),
|
||||
inserted: true,
|
||||
}))
|
||||
.with(FieldType.TEXT, FieldType.NUMBER, FieldType.RADIO, FieldType.CHECKBOX, () => ({
|
||||
...field,
|
||||
customText: form.getValues('customText'),
|
||||
inserted: true,
|
||||
}))
|
||||
.with(FieldType.SIGNATURE, () => {
|
||||
const value = form.getValues('signature');
|
||||
|
||||
return {
|
||||
...field,
|
||||
value,
|
||||
Signature: {
|
||||
id: -1,
|
||||
recipientId: -1,
|
||||
fieldId: -1,
|
||||
created: new Date(),
|
||||
signatureImageAsBase64: value,
|
||||
typedSignature: null,
|
||||
},
|
||||
inserted: true,
|
||||
};
|
||||
})
|
||||
.otherwise(() => {
|
||||
throw new Error('Unsupported field');
|
||||
});
|
||||
};
|
||||
|
||||
const insertField = (field: Field) => async () => {
|
||||
const isFieldFormValid = await validateFieldForm(field.type);
|
||||
if (!isFieldFormValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalFields((prev) =>
|
||||
prev.map((prevField) => {
|
||||
if (prevField.id !== field.id) {
|
||||
return prevField;
|
||||
}
|
||||
|
||||
return insertFormValueIntoField(field);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* When a form value changes, reset all the corresponding fields to be uninserted.
|
||||
*/
|
||||
const onFormValueChange = (fieldType: FieldType) => {
|
||||
setLocalFields((fields) =>
|
||||
fields.map((field) => {
|
||||
if (field.type !== fieldType) {
|
||||
return field;
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
inserted: false,
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocumentFlowFormContainerHeader
|
||||
title={documentFlow.title}
|
||||
description={documentFlow.description}
|
||||
/>
|
||||
|
||||
<Form {...form}>
|
||||
<fieldset className="flex h-full flex-col" disabled={form.formState.isSubmitting}>
|
||||
<DocumentFlowFormContainerContent>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>
|
||||
<Trans>Email</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="bg-background"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
{...field}
|
||||
onChange={(value) => {
|
||||
onFormValueChange(FieldType.EMAIL);
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{requireName && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required={requireName}>
|
||||
<Trans>Name</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="bg-background"
|
||||
{...field}
|
||||
onChange={(value) => {
|
||||
onFormValueChange(FieldType.NAME);
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{requireSignature && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="signature"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required={requireSignature}>
|
||||
<Trans>Signature</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Card
|
||||
className={cn('mt-2', {
|
||||
'rounded-sm ring-2 ring-red-500 ring-offset-2 transition-all':
|
||||
form.formState.errors.signature,
|
||||
})}
|
||||
gradient={!form.formState.errors.signature}
|
||||
degrees={-120}
|
||||
>
|
||||
<CardContent className="p-0">
|
||||
<SignaturePad
|
||||
className="h-44 w-full"
|
||||
defaultValue={field.value}
|
||||
onBlur={field.onBlur}
|
||||
onChange={(value) => {
|
||||
onFormValueChange(FieldType.SIGNATURE);
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{requireCustomText && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="customText"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required={requireCustomText}>
|
||||
<Trans>Custom Text</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="bg-background"
|
||||
{...field}
|
||||
onChange={(value) => {
|
||||
onFormValueChange(FieldType.TEXT);
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DocumentFlowFormContainerContent>
|
||||
|
||||
<DocumentFlowFormContainerFooter>
|
||||
<DocumentFlowFormContainerStep step={currentStep} maxStep={totalSteps} />
|
||||
|
||||
<DocumentFlowFormContainerActions
|
||||
loading={form.formState.isSubmitting}
|
||||
disabled={form.formState.isSubmitting}
|
||||
onGoBackClick={documentFlow.onBackStep}
|
||||
onGoNextClick={form.handleSubmit(onValidateFields)}
|
||||
/>
|
||||
</DocumentFlowFormContainerFooter>
|
||||
</fieldset>
|
||||
|
||||
{validateUninsertedFields && uninsertedFields[0] && (
|
||||
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
|
||||
<Trans>Click to insert field</Trans>
|
||||
</FieldToolTip>
|
||||
)}
|
||||
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{localFields.map((field) =>
|
||||
match(field.type)
|
||||
.with(
|
||||
FieldType.DATE,
|
||||
FieldType.TEXT,
|
||||
FieldType.EMAIL,
|
||||
FieldType.NAME,
|
||||
FieldType.NUMBER,
|
||||
FieldType.CHECKBOX,
|
||||
FieldType.RADIO,
|
||||
FieldType.DROPDOWN,
|
||||
() => {
|
||||
return (
|
||||
<SinglePlayerModeCustomTextField
|
||||
onClick={insertField(field)}
|
||||
key={field.id}
|
||||
field={field}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)
|
||||
.with(FieldType.SIGNATURE, () => (
|
||||
<SinglePlayerModeSignatureField
|
||||
onClick={insertField(field)}
|
||||
key={field.id}
|
||||
field={field}
|
||||
/>
|
||||
))
|
||||
.otherwise(() => {
|
||||
return null;
|
||||
}),
|
||||
)}
|
||||
</ElementVisible>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,18 +0,0 @@
|
||||
import { msg } from '@lingui/macro';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZAddSignatureFormSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, { message: msg`Email is required`.id })
|
||||
.email({ message: msg`Invalid email address`.id }),
|
||||
name: z.string(),
|
||||
customText: z.string(),
|
||||
number: z.number().optional(),
|
||||
radio: z.string().optional(),
|
||||
checkbox: z.boolean().optional(),
|
||||
dropdown: z.string().optional(),
|
||||
signature: z.string(),
|
||||
});
|
||||
|
||||
export type TAddSignatureFormSchema = z.infer<typeof ZAddSignatureFormSchema>;
|
||||
@ -41,6 +41,7 @@ import {
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
import { ShowFieldItem } from './show-field-item';
|
||||
import { SigningOrderConfirmation } from './signing-order-confirmation';
|
||||
import type { DocumentFlowStep } from './types';
|
||||
|
||||
export type AddSignersFormProps = {
|
||||
@ -123,6 +124,7 @@ export const AddSignersFormPartial = ({
|
||||
}, [recipients, form]);
|
||||
|
||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(alwaysShowAdvancedSettings);
|
||||
const [showSigningOrderConfirmation, setShowSigningOrderConfirmation] = useState(false);
|
||||
|
||||
const {
|
||||
setValue,
|
||||
@ -134,6 +136,10 @@ export const AddSignersFormPartial = ({
|
||||
const watchedSigners = watch('signers');
|
||||
const isSigningOrderSequential = watch('signingOrder') === DocumentSigningOrder.SEQUENTIAL;
|
||||
|
||||
const hasAssistantRole = useMemo(() => {
|
||||
return watchedSigners.some((signer) => signer.role === RecipientRole.ASSISTANT);
|
||||
}, [watchedSigners]);
|
||||
|
||||
const normalizeSigningOrders = (signers: typeof watchedSigners) => {
|
||||
return signers
|
||||
.sort((a, b) => (a.signingOrder ?? 0) - (b.signingOrder ?? 0))
|
||||
@ -233,6 +239,7 @@ export const AddSignersFormPartial = ({
|
||||
const items = Array.from(watchedSigners);
|
||||
const [reorderedSigner] = items.splice(result.source.index, 1);
|
||||
|
||||
// Find next valid position
|
||||
let insertIndex = result.destination.index;
|
||||
while (insertIndex < items.length && !canRecipientBeModified(items[insertIndex].nativeId)) {
|
||||
insertIndex++;
|
||||
@ -240,126 +247,116 @@ export const AddSignersFormPartial = ({
|
||||
|
||||
items.splice(insertIndex, 0, reorderedSigner);
|
||||
|
||||
const updatedSigners = items.map((item, index) => ({
|
||||
...item,
|
||||
signingOrder: !canRecipientBeModified(item.nativeId) ? item.signingOrder : index + 1,
|
||||
const updatedSigners = items.map((signer, index) => ({
|
||||
...signer,
|
||||
signingOrder: !canRecipientBeModified(signer.nativeId) ? signer.signingOrder : index + 1,
|
||||
}));
|
||||
|
||||
updatedSigners.forEach((item, index) => {
|
||||
const keys: (keyof typeof item)[] = [
|
||||
'formId',
|
||||
'nativeId',
|
||||
'email',
|
||||
'name',
|
||||
'role',
|
||||
'signingOrder',
|
||||
'actionAuth',
|
||||
];
|
||||
keys.forEach((key) => {
|
||||
form.setValue(`signers.${index}.${key}` as const, item[key]);
|
||||
});
|
||||
});
|
||||
form.setValue('signers', updatedSigners);
|
||||
|
||||
const currentLength = form.getValues('signers').length;
|
||||
if (currentLength > updatedSigners.length) {
|
||||
for (let i = updatedSigners.length; i < currentLength; i++) {
|
||||
form.unregister(`signers.${i}`);
|
||||
}
|
||||
const lastSigner = updatedSigners[updatedSigners.length - 1];
|
||||
if (lastSigner.role === RecipientRole.ASSISTANT) {
|
||||
toast({
|
||||
title: _(msg`Warning: Assistant as last signer`),
|
||||
description: _(
|
||||
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
await form.trigger('signers');
|
||||
},
|
||||
[form, canRecipientBeModified, watchedSigners],
|
||||
[form, canRecipientBeModified, watchedSigners, toast],
|
||||
);
|
||||
|
||||
const triggerDragAndDrop = useCallback(
|
||||
(fromIndex: number, toIndex: number) => {
|
||||
if (!$sensorApi.current) {
|
||||
const handleRoleChange = useCallback(
|
||||
(index: number, role: RecipientRole) => {
|
||||
const currentSigners = form.getValues('signers');
|
||||
const signingOrder = form.getValues('signingOrder');
|
||||
|
||||
// Handle parallel to sequential conversion for assistants
|
||||
if (role === RecipientRole.ASSISTANT && signingOrder === DocumentSigningOrder.PARALLEL) {
|
||||
form.setValue('signingOrder', DocumentSigningOrder.SEQUENTIAL);
|
||||
toast({
|
||||
title: _(msg`Signing order is enabled.`),
|
||||
description: _(msg`You cannot add assistants when signing order is disabled.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const draggableId = signers[fromIndex].id;
|
||||
const updatedSigners = currentSigners.map((signer, idx) => ({
|
||||
...signer,
|
||||
role: idx === index ? role : signer.role,
|
||||
signingOrder: !canRecipientBeModified(signer.nativeId) ? signer.signingOrder : idx + 1,
|
||||
}));
|
||||
|
||||
const preDrag = $sensorApi.current.tryGetLock(draggableId);
|
||||
form.setValue('signers', updatedSigners);
|
||||
|
||||
if (!preDrag) {
|
||||
return;
|
||||
if (role === RecipientRole.ASSISTANT && index === updatedSigners.length - 1) {
|
||||
toast({
|
||||
title: _(msg`Warning: Assistant as last signer`),
|
||||
description: _(
|
||||
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const drag = preDrag.snapLift();
|
||||
|
||||
setTimeout(() => {
|
||||
// Move directly to the target index
|
||||
if (fromIndex < toIndex) {
|
||||
for (let i = fromIndex; i < toIndex; i++) {
|
||||
drag.moveDown();
|
||||
}
|
||||
} else {
|
||||
for (let i = fromIndex; i > toIndex; i--) {
|
||||
drag.moveUp();
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
drag.drop();
|
||||
}, 500);
|
||||
}, 0);
|
||||
},
|
||||
[signers],
|
||||
);
|
||||
|
||||
const updateSigningOrders = useCallback(
|
||||
(newIndex: number, oldIndex: number) => {
|
||||
const updatedSigners = form.getValues('signers').map((signer, index) => {
|
||||
if (index === oldIndex) {
|
||||
return { ...signer, signingOrder: newIndex + 1 };
|
||||
} else if (index >= newIndex && index < oldIndex) {
|
||||
return {
|
||||
...signer,
|
||||
signingOrder: !canRecipientBeModified(signer.nativeId)
|
||||
? signer.signingOrder
|
||||
: (signer.signingOrder ?? index + 1) + 1,
|
||||
};
|
||||
} else if (index <= newIndex && index > oldIndex) {
|
||||
return {
|
||||
...signer,
|
||||
signingOrder: !canRecipientBeModified(signer.nativeId)
|
||||
? signer.signingOrder
|
||||
: Math.max(1, (signer.signingOrder ?? index + 1) - 1),
|
||||
};
|
||||
}
|
||||
return signer;
|
||||
});
|
||||
|
||||
updatedSigners.forEach((signer, index) => {
|
||||
form.setValue(`signers.${index}.signingOrder`, signer.signingOrder);
|
||||
});
|
||||
},
|
||||
[form, canRecipientBeModified],
|
||||
[form, toast, canRecipientBeModified],
|
||||
);
|
||||
|
||||
const handleSigningOrderChange = useCallback(
|
||||
(index: number, newOrderString: string) => {
|
||||
const newOrder = parseInt(newOrderString, 10);
|
||||
|
||||
if (!newOrderString.trim()) {
|
||||
const trimmedOrderString = newOrderString.trim();
|
||||
if (!trimmedOrderString) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Number.isNaN(newOrder)) {
|
||||
form.setValue(`signers.${index}.signingOrder`, index + 1);
|
||||
const newOrder = Number(trimmedOrderString);
|
||||
if (!Number.isInteger(newOrder) || newOrder < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newIndex = newOrder - 1;
|
||||
if (index !== newIndex) {
|
||||
updateSigningOrders(newIndex, index);
|
||||
triggerDragAndDrop(index, newIndex);
|
||||
const currentSigners = form.getValues('signers');
|
||||
const signer = currentSigners[index];
|
||||
|
||||
// Remove signer from current position and insert at new position
|
||||
const remainingSigners = currentSigners.filter((_, idx) => idx !== index);
|
||||
const newPosition = Math.min(Math.max(0, newOrder - 1), currentSigners.length - 1);
|
||||
remainingSigners.splice(newPosition, 0, signer);
|
||||
|
||||
const updatedSigners = remainingSigners.map((s, idx) => ({
|
||||
...s,
|
||||
signingOrder: !canRecipientBeModified(s.nativeId) ? s.signingOrder : idx + 1,
|
||||
}));
|
||||
|
||||
form.setValue('signers', updatedSigners);
|
||||
|
||||
if (signer.role === RecipientRole.ASSISTANT && newPosition === remainingSigners.length - 1) {
|
||||
toast({
|
||||
title: _(msg`Warning: Assistant as last signer`),
|
||||
description: _(
|
||||
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
[form, triggerDragAndDrop, updateSigningOrders],
|
||||
[form, canRecipientBeModified, toast],
|
||||
);
|
||||
|
||||
const handleSigningOrderDisable = useCallback(() => {
|
||||
setShowSigningOrderConfirmation(false);
|
||||
|
||||
const currentSigners = form.getValues('signers');
|
||||
const updatedSigners = currentSigners.map((signer) => ({
|
||||
...signer,
|
||||
role: signer.role === RecipientRole.ASSISTANT ? RecipientRole.SIGNER : signer.role,
|
||||
}));
|
||||
|
||||
form.setValue('signers', updatedSigners);
|
||||
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL);
|
||||
}, [form]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocumentFlowFormContainerHeader
|
||||
@ -383,13 +380,17 @@ export const AddSignersFormPartial = ({
|
||||
<Checkbox
|
||||
{...field}
|
||||
id="signingOrder"
|
||||
checkClassName="text-white"
|
||||
checked={field.value === DocumentSigningOrder.SEQUENTIAL}
|
||||
onCheckedChange={(checked) =>
|
||||
onCheckedChange={(checked) => {
|
||||
if (!checked && hasAssistantRole) {
|
||||
setShowSigningOrderConfirmation(true);
|
||||
return;
|
||||
}
|
||||
|
||||
field.onChange(
|
||||
checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL,
|
||||
)
|
||||
}
|
||||
);
|
||||
}}
|
||||
disabled={isSubmitting || hasDocumentBeenSent}
|
||||
/>
|
||||
</FormControl>
|
||||
@ -614,7 +615,11 @@ export const AddSignersFormPartial = ({
|
||||
<FormControl>
|
||||
<RecipientRoleSelect
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
isAssistantEnabled={isSigningOrderSequential}
|
||||
onValueChange={(value) =>
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
handleRoleChange(index, value as RecipientRole)
|
||||
}
|
||||
disabled={
|
||||
snapshot.isDragging ||
|
||||
isSubmitting ||
|
||||
@ -697,7 +702,6 @@ export const AddSignersFormPartial = ({
|
||||
<Checkbox
|
||||
id="showAdvancedRecipientSettings"
|
||||
className="h-5 w-5"
|
||||
checkClassName="dark:text-white text-primary"
|
||||
checked={showAdvancedSettings}
|
||||
onCheckedChange={(value) => setShowAdvancedSettings(Boolean(value))}
|
||||
/>
|
||||
@ -712,6 +716,12 @@ export const AddSignersFormPartial = ({
|
||||
)}
|
||||
</Form>
|
||||
</AnimateGenericFadeInOut>
|
||||
|
||||
<SigningOrderConfirmation
|
||||
open={showSigningOrderConfirmation}
|
||||
onOpenChange={setShowSigningOrderConfirmation}
|
||||
onConfirm={handleSigningOrderDisable}
|
||||
/>
|
||||
</DocumentFlowFormContainerContent>
|
||||
|
||||
<DocumentFlowFormContainerFooter>
|
||||
|
||||
@ -7,6 +7,7 @@ import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import type { TDocument } from '@documenso/lib/types/document';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { formatSigningLink } from '@documenso/lib/utils/recipients';
|
||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||
@ -15,7 +16,6 @@ import {
|
||||
DocumentStatus,
|
||||
RecipientRole,
|
||||
} from '@documenso/prisma/client';
|
||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||
|
||||
@ -43,7 +43,7 @@ export type AddSubjectFormProps = {
|
||||
documentFlow: DocumentFlowStep;
|
||||
recipients: Recipient[];
|
||||
fields: Field[];
|
||||
document: DocumentWithData;
|
||||
document: TDocument;
|
||||
onSubmit: (_data: TAddSubjectFormSchema) => void;
|
||||
isDocumentPdfLoaded: boolean;
|
||||
};
|
||||
|
||||
@ -33,12 +33,11 @@ export const CheckboxField = ({ field }: CheckboxFieldProps) => {
|
||||
parsedFieldMeta.values.map((item: { value: string; checked: boolean }, index: number) => (
|
||||
<div key={index} className="flex items-center gap-x-1.5">
|
||||
<Checkbox
|
||||
className="h-3 w-3"
|
||||
checkClassName="text-white"
|
||||
className="dark:border-field-border h-3 w-3 bg-white"
|
||||
id={`checkbox-${index}`}
|
||||
checked={item.checked}
|
||||
/>
|
||||
<Label htmlFor={`checkbox-${index}`} className="text-xs">
|
||||
<Label htmlFor={`checkbox-${index}`} className="text-xs font-normal text-black">
|
||||
{item.value}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
@ -34,12 +34,12 @@ export const RadioField = ({ field }: RadioFieldProps) => {
|
||||
{parsedFieldMeta.values?.map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-x-1.5">
|
||||
<RadioGroupItem
|
||||
className="pointer-events-none h-3 w-3"
|
||||
className="dark:border-field-border pointer-events-none h-3 w-3"
|
||||
value={item.value}
|
||||
id={`option-${index}`}
|
||||
checked={item.checked}
|
||||
/>
|
||||
<Label htmlFor={`option-${index}`} className="text-xs">
|
||||
<Label htmlFor={`option-${index}`} className="text-xs font-normal text-black">
|
||||
{item.value}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
@ -59,10 +59,10 @@ export const FieldIcon = ({
|
||||
if (fieldMeta && (type === 'TEXT' || type === 'NUMBER')) {
|
||||
if (type === 'TEXT' && 'text' in fieldMeta && fieldMeta.text && !fieldMeta.label) {
|
||||
label =
|
||||
fieldMeta.text.length > 10 ? fieldMeta.text.substring(0, 10) + '...' : fieldMeta.text;
|
||||
fieldMeta.text.length > 20 ? fieldMeta.text.substring(0, 20) + '...' : fieldMeta.text;
|
||||
} else if (fieldMeta.label) {
|
||||
label =
|
||||
fieldMeta.label.length > 10 ? fieldMeta.label.substring(0, 10) + '...' : fieldMeta.label;
|
||||
fieldMeta.label.length > 20 ? fieldMeta.label.substring(0, 20) + '...' : fieldMeta.label;
|
||||
} else {
|
||||
label = fieldIcons[type]?.label;
|
||||
}
|
||||
|
||||
@ -2,9 +2,6 @@
|
||||
|
||||
import { forwardRef, useEffect, useState } from 'react';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
@ -25,7 +22,6 @@ import {
|
||||
ZFieldMetaSchema,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import type { FieldFormType } from './add-fields';
|
||||
@ -75,21 +71,25 @@ const getDefaultState = (fieldType: FieldType): FieldMeta => {
|
||||
return {
|
||||
type: 'initials',
|
||||
fontSize: 14,
|
||||
textAlign: 'left',
|
||||
};
|
||||
case FieldType.NAME:
|
||||
return {
|
||||
type: 'name',
|
||||
fontSize: 14,
|
||||
textAlign: 'left',
|
||||
};
|
||||
case FieldType.EMAIL:
|
||||
return {
|
||||
type: 'email',
|
||||
fontSize: 14,
|
||||
textAlign: 'left',
|
||||
};
|
||||
case FieldType.DATE:
|
||||
return {
|
||||
type: 'date',
|
||||
fontSize: 14,
|
||||
textAlign: 'left',
|
||||
};
|
||||
case FieldType.TEXT:
|
||||
return {
|
||||
@ -101,6 +101,7 @@ const getDefaultState = (fieldType: FieldType): FieldMeta => {
|
||||
fontSize: 14,
|
||||
required: false,
|
||||
readOnly: false,
|
||||
textAlign: 'left',
|
||||
};
|
||||
case FieldType.NUMBER:
|
||||
return {
|
||||
@ -114,6 +115,7 @@ const getDefaultState = (fieldType: FieldType): FieldMeta => {
|
||||
required: false,
|
||||
readOnly: false,
|
||||
fontSize: 14,
|
||||
textAlign: 'left',
|
||||
};
|
||||
case FieldType.RADIO:
|
||||
return {
|
||||
@ -146,49 +148,14 @@ const getDefaultState = (fieldType: FieldType): FieldMeta => {
|
||||
|
||||
export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSettingsProps>(
|
||||
(
|
||||
{
|
||||
title,
|
||||
description,
|
||||
field,
|
||||
fields,
|
||||
onAdvancedSettings,
|
||||
isDocumentPdfLoaded = true,
|
||||
onSave,
|
||||
teamId,
|
||||
},
|
||||
{ title, description, field, fields, onAdvancedSettings, isDocumentPdfLoaded = true, onSave },
|
||||
ref,
|
||||
) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const params = useParams();
|
||||
const pathname = usePathname();
|
||||
const id = params?.id;
|
||||
const isTemplatePage = pathname?.includes('template');
|
||||
const isDocumentPage = pathname?.includes('document');
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
|
||||
const { data: template } = trpc.template.getTemplateWithDetailsById.useQuery(
|
||||
{
|
||||
id: Number(id),
|
||||
},
|
||||
{
|
||||
enabled: isTemplatePage,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: document } = trpc.document.getDocumentById.useQuery(
|
||||
{
|
||||
id: Number(id),
|
||||
teamId,
|
||||
},
|
||||
{
|
||||
enabled: isDocumentPage,
|
||||
},
|
||||
);
|
||||
|
||||
const doesFieldExist = (!!document || !!template) && field.nativeId !== undefined;
|
||||
|
||||
const fieldMeta = field?.fieldMeta;
|
||||
|
||||
const localStorageKey = `field_${field.formId}_${field.type}`;
|
||||
|
||||
@ -47,6 +47,9 @@ export type FieldItemProps = {
|
||||
recipientIndex?: number;
|
||||
hideRecipients?: boolean;
|
||||
hasErrors?: boolean;
|
||||
active?: boolean;
|
||||
onFieldActivate?: () => void;
|
||||
onFieldDeactivate?: () => void;
|
||||
};
|
||||
|
||||
export const FieldItem = ({
|
||||
@ -67,8 +70,10 @@ export const FieldItem = ({
|
||||
recipientIndex = 0,
|
||||
hideRecipients = false,
|
||||
hasErrors,
|
||||
active,
|
||||
onFieldActivate,
|
||||
onFieldDeactivate,
|
||||
}: FieldItemProps) => {
|
||||
const [active, setActive] = useState(false);
|
||||
const [coords, setCoords] = useState({
|
||||
pageX: 0,
|
||||
pageY: 0,
|
||||
@ -150,6 +155,8 @@ export const FieldItem = ({
|
||||
});
|
||||
|
||||
if (isOutsideOfField) {
|
||||
setSettingsActive(false);
|
||||
onFieldDeactivate?.();
|
||||
onBlur?.();
|
||||
}
|
||||
};
|
||||
@ -188,10 +195,12 @@ export const FieldItem = ({
|
||||
return createPortal(
|
||||
<Rnd
|
||||
key={coords.pageX + coords.pageY + coords.pageHeight + coords.pageWidth}
|
||||
className={cn('group z-20', {
|
||||
className={cn('group', {
|
||||
'pointer-events-none': passive,
|
||||
'pointer-events-none cursor-not-allowed opacity-75': disabled,
|
||||
'z-10': !active || disabled,
|
||||
'z-50': active && !disabled,
|
||||
'z-20': !active && !disabled,
|
||||
'z-10': disabled,
|
||||
})}
|
||||
minHeight={fixedSize ? '' : minHeight || 'auto'}
|
||||
minWidth={fixedSize ? '' : minWidth || 'auto'}
|
||||
@ -202,15 +211,21 @@ export const FieldItem = ({
|
||||
width: fixedSize ? '' : coords.pageWidth,
|
||||
}}
|
||||
bounds={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`}
|
||||
onDragStart={() => setActive(true)}
|
||||
onResizeStart={() => setActive(true)}
|
||||
onDragStart={() => onFieldActivate?.()}
|
||||
onResizeStart={() => onFieldActivate?.()}
|
||||
enableResizing={!fixedSize}
|
||||
resizeHandleStyles={{
|
||||
bottom: { bottom: -8, cursor: 'ns-resize' },
|
||||
top: { top: -8, cursor: 'ns-resize' },
|
||||
left: { cursor: 'ew-resize' },
|
||||
right: { cursor: 'ew-resize' },
|
||||
}}
|
||||
onResizeStop={(_e, _d, ref) => {
|
||||
setActive(false);
|
||||
onFieldDeactivate?.();
|
||||
onResize?.(ref);
|
||||
}}
|
||||
onDragStop={(_e, d) => {
|
||||
setActive(false);
|
||||
onFieldDeactivate?.();
|
||||
onMove?.(d.node);
|
||||
}}
|
||||
>
|
||||
@ -226,8 +241,10 @@ export const FieldItem = ({
|
||||
!fixedSize && '[container-type:size]',
|
||||
)}
|
||||
data-error={hasErrors ? 'true' : undefined}
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSettingsActive((prev) => !prev);
|
||||
onFieldActivate?.();
|
||||
onFocus?.();
|
||||
}}
|
||||
ref={$el}
|
||||
@ -264,7 +281,7 @@ export const FieldItem = ({
|
||||
</div>
|
||||
|
||||
{!disabled && settingsActive && (
|
||||
<div className="mt-1 flex justify-center">
|
||||
<div className="z-[60] mt-1 flex justify-center">
|
||||
<div className="dark:bg-background group flex items-center justify-evenly gap-x-1 rounded-md border bg-gray-900 p-0.5">
|
||||
{advancedField && (
|
||||
<button
|
||||
|
||||
@ -205,8 +205,7 @@ export const CheckboxFieldAdvancedSettings = ({
|
||||
{values.map((value, index) => (
|
||||
<div key={index} className="mt-2 flex items-center gap-4">
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-documenso border-foreground/30 h-5 w-5"
|
||||
checkClassName="text-white"
|
||||
className="data-[state=checked]:bg-primary border-foreground/30 h-5 w-5"
|
||||
checked={value.checked}
|
||||
onCheckedChange={(checked) => handleCheckboxValue(index, 'checked', checked)}
|
||||
/>
|
||||
@ -217,7 +216,7 @@ export const CheckboxFieldAdvancedSettings = ({
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="col-span-1 mt-auto inline-flex h-10 w-10 items-center text-slate-500 hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className="col-span-1 mt-auto inline-flex h-10 w-10 items-center text-slate-500 hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
onClick={() => removeValue(index)}
|
||||
>
|
||||
<Trash className="h-5 w-5" />
|
||||
|
||||
@ -5,6 +5,13 @@ import { validateFields as validateDateFields } from '@documenso/lib/advanced-fi
|
||||
import { type TDateFieldMeta as DateFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
|
||||
type DateFieldAdvancedSettingsProps = {
|
||||
fieldState: DateFieldMeta;
|
||||
@ -66,6 +73,27 @@ export const DateFieldAdvancedSettings = ({
|
||||
max={96}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>
|
||||
<Trans>Text Align</Trans>
|
||||
</Label>
|
||||
|
||||
<Select
|
||||
value={fieldState.textAlign}
|
||||
onValueChange={(value) => handleInput('textAlign', value)}
|
||||
>
|
||||
<SelectTrigger className="bg-background mt-2">
|
||||
<SelectValue placeholder="Select text align" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="left">Left</SelectItem>
|
||||
<SelectItem value="center">Center</SelectItem>
|
||||
<SelectItem value="right">Right</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -5,6 +5,13 @@ import { validateFields as validateEmailFields } from '@documenso/lib/advanced-f
|
||||
import { type TEmailFieldMeta as EmailFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
|
||||
type EmailFieldAdvancedSettingsProps = {
|
||||
fieldState: EmailFieldMeta;
|
||||
@ -48,6 +55,27 @@ export const EmailFieldAdvancedSettings = ({
|
||||
max={96}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>
|
||||
<Trans>Text Align</Trans>
|
||||
</Label>
|
||||
|
||||
<Select
|
||||
value={fieldState.textAlign}
|
||||
onValueChange={(value) => handleInput('textAlign', value)}
|
||||
>
|
||||
<SelectTrigger className="bg-background mt-2">
|
||||
<SelectValue placeholder="Select text align" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="left">Left</SelectItem>
|
||||
<SelectItem value="center">Center</SelectItem>
|
||||
<SelectItem value="right">Right</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -6,6 +6,8 @@ import { type TInitialsFieldMeta as InitialsFieldMeta } from '@documenso/lib/typ
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../select';
|
||||
|
||||
type InitialsFieldAdvancedSettingsProps = {
|
||||
fieldState: InitialsFieldMeta;
|
||||
handleFieldChange: (key: keyof InitialsFieldMeta, value: string | boolean) => void;
|
||||
@ -48,6 +50,27 @@ export const InitialsFieldAdvancedSettings = ({
|
||||
max={96}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>
|
||||
<Trans>Text Align</Trans>
|
||||
</Label>
|
||||
|
||||
<Select
|
||||
value={fieldState.textAlign}
|
||||
onValueChange={(value) => handleInput('textAlign', value)}
|
||||
>
|
||||
<SelectTrigger className="bg-background mt-2">
|
||||
<SelectValue placeholder="Select text align" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="left">Left</SelectItem>
|
||||
<SelectItem value="center">Center</SelectItem>
|
||||
<SelectItem value="right">Right</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -5,6 +5,13 @@ import { validateFields as validateNameFields } from '@documenso/lib/advanced-fi
|
||||
import { type TNameFieldMeta as NameFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
|
||||
type NameFieldAdvancedSettingsProps = {
|
||||
fieldState: NameFieldMeta;
|
||||
@ -48,6 +55,27 @@ export const NameFieldAdvancedSettings = ({
|
||||
max={96}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>
|
||||
<Trans>Text Align</Trans>
|
||||
</Label>
|
||||
|
||||
<Select
|
||||
value={fieldState.textAlign}
|
||||
onValueChange={(value) => handleInput('textAlign', value)}
|
||||
>
|
||||
<SelectTrigger className="bg-background mt-2">
|
||||
<SelectValue placeholder="Select text align" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="left">Left</SelectItem>
|
||||
<SelectItem value="center">Center</SelectItem>
|
||||
<SelectItem value="right">Right</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -38,12 +38,12 @@ export const NumberFieldAdvancedSettings = ({
|
||||
const [showValidation, setShowValidation] = useState(false);
|
||||
|
||||
const handleInput = (field: keyof NumberFieldMeta, value: string | boolean) => {
|
||||
const userValue = field === 'value' ? value : fieldState.value ?? 0;
|
||||
const userValue = field === 'value' ? value : (fieldState.value ?? 0);
|
||||
const userMinValue = field === 'minValue' ? Number(value) : Number(fieldState.minValue ?? 0);
|
||||
const userMaxValue = field === 'maxValue' ? Number(value) : Number(fieldState.maxValue ?? 0);
|
||||
const readOnly = field === 'readOnly' ? Boolean(value) : Boolean(fieldState.readOnly);
|
||||
const required = field === 'required' ? Boolean(value) : Boolean(fieldState.required);
|
||||
const numberFormat = field === 'numberFormat' ? String(value) : fieldState.numberFormat ?? '';
|
||||
const numberFormat = field === 'numberFormat' ? String(value) : (fieldState.numberFormat ?? '');
|
||||
const fontSize = field === 'fontSize' ? Number(value) : Number(fieldState.fontSize ?? 14);
|
||||
|
||||
const valueErrors = validateNumberField(String(userValue), {
|
||||
@ -135,6 +135,27 @@ export const NumberFieldAdvancedSettings = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>
|
||||
<Trans>Text Align</Trans>
|
||||
</Label>
|
||||
|
||||
<Select
|
||||
value={fieldState.textAlign}
|
||||
onValueChange={(value) => handleInput('textAlign', value)}
|
||||
>
|
||||
<SelectTrigger className="bg-background mt-2">
|
||||
<SelectValue placeholder="Select text align" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="left">Left</SelectItem>
|
||||
<SelectItem value="center">Center</SelectItem>
|
||||
<SelectItem value="right">Right</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex flex-col gap-4">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Switch
|
||||
|
||||
@ -141,7 +141,7 @@ export const RadioFieldAdvancedSettings = ({
|
||||
{values.map((value) => (
|
||||
<div key={value.id} className="mt-2 flex items-center gap-4">
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-documenso border-foreground/30 data-[state=checked]:ring-documenso dark:data-[state=checked]:ring-offset-background h-5 w-5 rounded-full data-[state=checked]:ring-1 data-[state=checked]:ring-offset-2 data-[state=checked]:ring-offset-white"
|
||||
className="data-[state=checked]:bg-documenso border-foreground/30 data-[state=checked]:ring-primary dark:data-[state=checked]:ring-offset-background h-5 w-5 rounded-full data-[state=checked]:ring-1 data-[state=checked]:ring-offset-2 data-[state=checked]:ring-offset-white"
|
||||
checked={value.checked}
|
||||
onCheckedChange={(checked) => handleCheckedChange(Boolean(checked), value.id)}
|
||||
/>
|
||||
|
||||
@ -5,6 +5,13 @@ import { validateTextField } from '@documenso/lib/advanced-fields-validation/val
|
||||
import { type TTextFieldMeta as TextFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
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 { Textarea } from '@documenso/ui/primitives/textarea';
|
||||
|
||||
@ -22,7 +29,7 @@ export const TextFieldAdvancedSettings = ({
|
||||
const { _ } = useLingui();
|
||||
|
||||
const handleInput = (field: keyof TextFieldMeta, value: string | boolean) => {
|
||||
const text = field === 'text' ? String(value) : fieldState.text ?? '';
|
||||
const text = field === 'text' ? String(value) : (fieldState.text ?? '');
|
||||
const limit =
|
||||
field === 'characterLimit' ? Number(value) : Number(fieldState.characterLimit ?? 0);
|
||||
const fontSize = field === 'fontSize' ? Number(value) : Number(fieldState.fontSize ?? 14);
|
||||
@ -112,6 +119,27 @@ export const TextFieldAdvancedSettings = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>
|
||||
<Trans>Text Align</Trans>
|
||||
</Label>
|
||||
|
||||
<Select
|
||||
value={fieldState.textAlign}
|
||||
onValueChange={(value) => handleInput('textAlign', value)}
|
||||
>
|
||||
<SelectTrigger className="bg-background mt-2">
|
||||
<SelectValue placeholder="Select text align" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="left">Left</SelectItem>
|
||||
<SelectItem value="center">Center</SelectItem>
|
||||
<SelectItem value="right">Right</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-4">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Switch
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@documenso/ui/primitives/alert-dialog';
|
||||
|
||||
export type SigningOrderConfirmationProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirm: () => void;
|
||||
};
|
||||
|
||||
export function SigningOrderConfirmation({
|
||||
open,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
}: SigningOrderConfirmationProps) {
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Warning</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
You have an assistant role on the signers list, removing the signing order will change
|
||||
the assistant role to signer.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onConfirm}>Proceed</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
@ -1,249 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Caveat } from 'next/font/google';
|
||||
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { CalendarDays, CheckSquare, ChevronDown, Disc, Hash, Mail, Type, User } from 'lucide-react';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useElementScaleSize } from '@documenso/lib/client-only/hooks/use-element-scale-size';
|
||||
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
|
||||
import {
|
||||
DEFAULT_HANDWRITING_FONT_SIZE,
|
||||
DEFAULT_STANDARD_FONT_SIZE,
|
||||
MIN_HANDWRITING_FONT_SIZE,
|
||||
MIN_STANDARD_FONT_SIZE,
|
||||
} from '@documenso/lib/constants/pdf';
|
||||
import type { Field } from '@documenso/prisma/client';
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||
|
||||
import { FieldRootContainer } from '../../components/field/field';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
const fontCaveat = Caveat({
|
||||
weight: ['500'],
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
variable: '--font-caveat',
|
||||
});
|
||||
|
||||
export type SinglePlayerModeFieldContainerProps = {
|
||||
field: FieldWithSignature;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export type SinglePlayerModeFieldProps<T> = {
|
||||
field: T;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export function SinglePlayerModeFieldCardContainer({
|
||||
field,
|
||||
children,
|
||||
}: SinglePlayerModeFieldContainerProps) {
|
||||
return (
|
||||
<FieldRootContainer field={field}>
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
<motion.div
|
||||
key={field.inserted ? 'inserted' : 'not-inserted'}
|
||||
className="flex items-center justify-center p-2"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
}}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: 'easeIn',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</FieldRootContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function SinglePlayerModeSignatureField({
|
||||
field,
|
||||
onClick,
|
||||
}: SinglePlayerModeFieldProps<FieldWithSignature>) {
|
||||
const fontVariable = '--font-signature';
|
||||
const fontVariableValue = getComputedStyle(document.documentElement).getPropertyValue(
|
||||
fontVariable,
|
||||
);
|
||||
|
||||
const minFontSize = MIN_HANDWRITING_FONT_SIZE;
|
||||
const maxFontSize = DEFAULT_HANDWRITING_FONT_SIZE;
|
||||
|
||||
if (!isSignatureFieldType(field.type)) {
|
||||
throw new Error('Invalid field type');
|
||||
}
|
||||
|
||||
const { height, width } = useFieldPageCoords(field);
|
||||
|
||||
const insertedBase64Signature = field.inserted && field.Signature?.signatureImageAsBase64;
|
||||
const insertedTypeSignature = field.inserted && field.Signature?.typedSignature;
|
||||
|
||||
const scalingFactor = useElementScaleSize(
|
||||
{
|
||||
height,
|
||||
width,
|
||||
},
|
||||
insertedTypeSignature || '',
|
||||
maxFontSize,
|
||||
fontVariableValue,
|
||||
);
|
||||
|
||||
const fontSize = maxFontSize * scalingFactor;
|
||||
|
||||
return (
|
||||
<SinglePlayerModeFieldCardContainer field={field}>
|
||||
{insertedBase64Signature ? (
|
||||
<img
|
||||
src={insertedBase64Signature}
|
||||
alt="Your signature"
|
||||
className="h-full max-w-full object-contain dark:invert"
|
||||
/>
|
||||
) : insertedTypeSignature ? (
|
||||
<p
|
||||
style={{
|
||||
fontSize: `clamp(${minFontSize}px, ${fontSize}px, ${maxFontSize}px)`,
|
||||
fontFamily: `var(${fontVariable})`,
|
||||
}}
|
||||
className="font-signature"
|
||||
>
|
||||
{insertedTypeSignature}
|
||||
</p>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => onClick?.()}
|
||||
className={cn(
|
||||
'group-hover:text-primary absolute inset-0 h-full w-full duration-200',
|
||||
fontCaveat.className,
|
||||
)}
|
||||
>
|
||||
<span className="text-muted-foreground truncate text-3xl font-medium ">Signature</span>
|
||||
</button>
|
||||
)}
|
||||
</SinglePlayerModeFieldCardContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function SinglePlayerModeCustomTextField({
|
||||
field,
|
||||
onClick,
|
||||
}: SinglePlayerModeFieldProps<Field>) {
|
||||
const fontVariable = '--font-sans';
|
||||
const fontVariableValue = getComputedStyle(document.documentElement).getPropertyValue(
|
||||
fontVariable,
|
||||
);
|
||||
|
||||
const minFontSize = MIN_STANDARD_FONT_SIZE;
|
||||
const maxFontSize = DEFAULT_STANDARD_FONT_SIZE;
|
||||
|
||||
if (isSignatureFieldType(field.type)) {
|
||||
throw new Error('Invalid field type');
|
||||
}
|
||||
|
||||
const $paragraphEl = useRef<HTMLParagraphElement>(null);
|
||||
|
||||
const { height, width } = useFieldPageCoords(field);
|
||||
|
||||
const scalingFactor = useElementScaleSize(
|
||||
{
|
||||
height,
|
||||
width,
|
||||
},
|
||||
field.customText,
|
||||
maxFontSize,
|
||||
fontVariableValue,
|
||||
);
|
||||
|
||||
const fontSize = maxFontSize * scalingFactor;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SinglePlayerModeFieldCardContainer field={field}>
|
||||
{field.inserted ? (
|
||||
<p
|
||||
ref={$paragraphEl}
|
||||
style={{
|
||||
fontSize: `clamp(${minFontSize}px, ${fontSize}px, ${maxFontSize}px)`,
|
||||
fontFamily: `var(${fontVariable})`,
|
||||
}}
|
||||
>
|
||||
{field.customText ??
|
||||
(field.fieldMeta && typeof field.fieldMeta === 'object' && 'label' in field.fieldMeta
|
||||
? field.fieldMeta.label
|
||||
: '')}
|
||||
</p>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => onClick?.()}
|
||||
className="group-hover:text-primary text-muted-foreground absolute inset-0 h-full w-full text-lg duration-200"
|
||||
>
|
||||
{match(field.type)
|
||||
.with(FieldType.DATE, () => (
|
||||
<div className="text-field-card-foreground flex items-center justify-center gap-x-1 text-xl font-light">
|
||||
<CalendarDays /> Date
|
||||
</div>
|
||||
))
|
||||
.with(FieldType.NAME, () => (
|
||||
<div className="text-field-card-foreground flex items-center justify-center gap-x-1 text-xl font-light">
|
||||
<User /> Name
|
||||
</div>
|
||||
))
|
||||
.with(FieldType.EMAIL, () => (
|
||||
<div className="text-field-card-foreground flex items-center justify-center gap-x-1 text-xl font-light">
|
||||
<Mail /> Email
|
||||
</div>
|
||||
))
|
||||
.with(FieldType.TEXT, () => (
|
||||
<div className="text-field-card-foreground flex items-center justify-center gap-x-1 text-xl font-light">
|
||||
<Type /> Text
|
||||
</div>
|
||||
))
|
||||
.with(FieldType.SIGNATURE, FieldType.FREE_SIGNATURE, () => (
|
||||
<div
|
||||
className={cn(
|
||||
'text-muted-foreground w-full truncate text-3xl font-medium',
|
||||
fontCaveat.className,
|
||||
)}
|
||||
>
|
||||
Signature
|
||||
</div>
|
||||
))
|
||||
.with(FieldType.NUMBER, () => (
|
||||
<div className="text-field-card-foreground flex items-center justify-center gap-x-1 text-xl font-light">
|
||||
<Hash /> Number
|
||||
</div>
|
||||
))
|
||||
.with(FieldType.CHECKBOX, () => (
|
||||
<div className="text-field-card-foreground flex items-center justify-center gap-x-1 text-xl font-light">
|
||||
<CheckSquare /> Checkbox
|
||||
</div>
|
||||
))
|
||||
.with(FieldType.RADIO, () => (
|
||||
<div className="text-field-card-foreground flex items-center justify-center gap-x-1 text-xl font-light">
|
||||
<Disc /> Radio
|
||||
</div>
|
||||
))
|
||||
.with(FieldType.DROPDOWN, () => (
|
||||
<div className="text-field-card-foreground flex items-center justify-center gap-x-1 text-xl font-light">
|
||||
<ChevronDown /> Dropdown
|
||||
</div>
|
||||
))
|
||||
.otherwise(() => '')}
|
||||
</button>
|
||||
)}
|
||||
</SinglePlayerModeFieldCardContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const isSignatureFieldType = (fieldType: Field['type']) =>
|
||||
fieldType === FieldType.SIGNATURE || fieldType === FieldType.FREE_SIGNATURE;
|
||||
Reference in New Issue
Block a user