fix: merge conflicts

This commit is contained in:
Ephraim Atta-Duncan
2025-02-06 11:47:44 +00:00
783 changed files with 20849 additions and 22470 deletions

View File

@ -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>

View File

@ -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"

View File

@ -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()

View File

@ -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>
</>
);
};

View File

@ -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>;

View File

@ -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>

View File

@ -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;
};

View File

@ -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>

View File

@ -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>

View File

@ -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;
}

View File

@ -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}`;

View File

@ -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

View File

@ -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" />

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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

View File

@ -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)}
/>

View File

@ -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

View File

@ -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>
);
}

View File

@ -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;