mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
Introduces the ability for users with the **Assistant** role to prefill fields on behalf of other signers. Assistants can fill in various field types such as text, checkboxes, dates, and more, streamlining the document preparation process before it reaches the final signers.
995 lines
37 KiB
TypeScript
995 lines
37 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
import { msg } from '@lingui/core/macro';
|
|
import { useLingui } from '@lingui/react';
|
|
import { Trans } from '@lingui/react/macro';
|
|
import type { Field, Recipient } from '@prisma/client';
|
|
import { FieldType, RecipientRole, SendStatus } from '@prisma/client';
|
|
import {
|
|
CalendarDays,
|
|
CheckSquare,
|
|
ChevronDown,
|
|
ChevronsUpDown,
|
|
Contact,
|
|
Disc,
|
|
Hash,
|
|
Mail,
|
|
Type,
|
|
User,
|
|
} from 'lucide-react';
|
|
import { useFieldArray, useForm } from 'react-hook-form';
|
|
import { useHotkeys } from 'react-hotkeys-hook';
|
|
|
|
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
|
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
|
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
|
import {
|
|
type TFieldMetaSchema as FieldMeta,
|
|
ZFieldMetaSchema,
|
|
} from '@documenso/lib/types/field-meta';
|
|
import { nanoid } from '@documenso/lib/universal/id';
|
|
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
|
|
import { cn } from '@documenso/ui/lib/utils';
|
|
import { Button } from '@documenso/ui/primitives/button';
|
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
|
import {
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
} from '@documenso/ui/primitives/command';
|
|
import {
|
|
DocumentFlowFormContainerActions,
|
|
DocumentFlowFormContainerContent,
|
|
DocumentFlowFormContainerFooter,
|
|
DocumentFlowFormContainerHeader,
|
|
DocumentFlowFormContainerStep,
|
|
} from '@documenso/ui/primitives/document-flow/document-flow-root';
|
|
import { FieldItem } from '@documenso/ui/primitives/document-flow/field-item';
|
|
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
|
|
import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types';
|
|
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
|
|
import { getSignerColorStyles, useSignerColors } from '../../lib/signer-colors';
|
|
import { Checkbox } from '../checkbox';
|
|
import type { FieldFormType } from '../document-flow/add-fields';
|
|
import { FieldAdvancedSettings } from '../document-flow/field-item-advanced-settings';
|
|
import { Form, FormControl, FormField, FormItem, FormLabel } from '../form/form';
|
|
import { useStep } from '../stepper';
|
|
import type { TAddTemplateFieldsFormSchema } from './add-template-fields.types';
|
|
|
|
const MIN_HEIGHT_PX = 12;
|
|
const MIN_WIDTH_PX = 36;
|
|
|
|
const DEFAULT_HEIGHT_PX = MIN_HEIGHT_PX * 2.5;
|
|
const DEFAULT_WIDTH_PX = MIN_WIDTH_PX * 2.5;
|
|
|
|
export type AddTemplateFieldsFormProps = {
|
|
documentFlow: DocumentFlowStep;
|
|
hideRecipients?: boolean;
|
|
recipients: Recipient[];
|
|
fields: Field[];
|
|
onSubmit: (_data: TAddTemplateFieldsFormSchema) => void;
|
|
teamId?: number;
|
|
typedSignatureEnabled?: boolean;
|
|
};
|
|
|
|
export const AddTemplateFieldsFormPartial = ({
|
|
documentFlow,
|
|
hideRecipients = false,
|
|
recipients,
|
|
fields,
|
|
onSubmit,
|
|
teamId,
|
|
typedSignatureEnabled,
|
|
}: AddTemplateFieldsFormProps) => {
|
|
const { _ } = useLingui();
|
|
const { toast } = useToast();
|
|
|
|
const { isWithinPageBounds, getFieldPosition, getPage } = useDocumentElement();
|
|
const { currentStep, totalSteps, previousStep } = useStep();
|
|
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
|
const [currentField, setCurrentField] = useState<FieldFormType>();
|
|
const [activeFieldId, setActiveFieldId] = useState<string | null>(null);
|
|
const [lastActiveField, setLastActiveField] = useState<
|
|
TAddTemplateFieldsFormSchema['fields'][0] | null
|
|
>(null);
|
|
const [fieldClipboard, setFieldClipboard] = useState<
|
|
TAddTemplateFieldsFormSchema['fields'][0] | null
|
|
>(null);
|
|
|
|
const form = useForm<TAddTemplateFieldsFormSchema>({
|
|
defaultValues: {
|
|
fields: fields.map((field) => ({
|
|
nativeId: field.id,
|
|
formId: `${field.id}-${field.templateId}`,
|
|
pageNumber: field.page,
|
|
type: field.type,
|
|
pageX: Number(field.positionX),
|
|
pageY: Number(field.positionY),
|
|
pageWidth: Number(field.width),
|
|
pageHeight: Number(field.height),
|
|
signerId: field.recipientId ?? -1,
|
|
signerEmail:
|
|
recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
|
|
signerToken:
|
|
recipients.find((recipient) => recipient.id === field.recipientId)?.token ?? '',
|
|
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
|
|
})),
|
|
typedSignatureEnabled: typedSignatureEnabled ?? false,
|
|
},
|
|
});
|
|
|
|
const onFormSubmit = form.handleSubmit(onSubmit);
|
|
|
|
const {
|
|
append,
|
|
remove,
|
|
update,
|
|
fields: localFields,
|
|
} = useFieldArray({
|
|
control: form.control,
|
|
name: 'fields',
|
|
});
|
|
|
|
const [selectedField, setSelectedField] = useState<FieldType | null>(null);
|
|
const [selectedSigner, setSelectedSigner] = useState<Recipient | null>(null);
|
|
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
|
|
|
|
const selectedSignerIndex = recipients.findIndex((r) => r.id === selectedSigner?.id);
|
|
const selectedSignerStyles = useSignerColors(
|
|
selectedSignerIndex === -1 ? 0 : selectedSignerIndex,
|
|
);
|
|
|
|
const onFieldCopy = useCallback(
|
|
(event?: KeyboardEvent | null, options?: { duplicate?: boolean }) => {
|
|
const { duplicate = false } = options ?? {};
|
|
|
|
if (lastActiveField) {
|
|
event?.preventDefault();
|
|
|
|
if (!duplicate) {
|
|
setFieldClipboard(lastActiveField);
|
|
|
|
toast({
|
|
title: 'Copied field',
|
|
description: 'Copied field to clipboard',
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
const newField: TAddTemplateFieldsFormSchema['fields'][0] = {
|
|
...structuredClone(lastActiveField),
|
|
formId: nanoid(12),
|
|
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
|
|
signerId: selectedSigner?.id ?? lastActiveField.signerId,
|
|
signerToken: selectedSigner?.token ?? lastActiveField.signerToken,
|
|
pageX: lastActiveField.pageX + 3,
|
|
pageY: lastActiveField.pageY + 3,
|
|
};
|
|
|
|
append(newField);
|
|
}
|
|
},
|
|
[
|
|
append,
|
|
lastActiveField,
|
|
selectedSigner?.email,
|
|
selectedSigner?.id,
|
|
selectedSigner?.token,
|
|
toast,
|
|
],
|
|
);
|
|
|
|
const onFieldPaste = useCallback(
|
|
(event: KeyboardEvent) => {
|
|
if (fieldClipboard) {
|
|
event.preventDefault();
|
|
|
|
const copiedField = structuredClone(fieldClipboard);
|
|
|
|
append({
|
|
...copiedField,
|
|
formId: nanoid(12),
|
|
signerEmail: selectedSigner?.email ?? copiedField.signerEmail,
|
|
signerId: selectedSigner?.id ?? copiedField.signerId,
|
|
signerToken: selectedSigner?.token ?? copiedField.signerToken,
|
|
pageX: copiedField.pageX + 3,
|
|
pageY: copiedField.pageY + 3,
|
|
});
|
|
}
|
|
},
|
|
[append, fieldClipboard, selectedSigner?.email, selectedSigner?.id, selectedSigner?.token],
|
|
);
|
|
|
|
useHotkeys(['ctrl+c', 'meta+c'], (evt) => onFieldCopy(evt));
|
|
useHotkeys(['ctrl+v', 'meta+v'], (evt) => onFieldPaste(evt));
|
|
useHotkeys(['ctrl+d', 'meta+d'], (evt) => onFieldCopy(evt, { duplicate: true }));
|
|
|
|
const filterFieldsWithEmptyValues = (fields: typeof localFields, fieldType: string) =>
|
|
fields
|
|
.filter((field) => field.type === fieldType)
|
|
.filter((field) => {
|
|
if (field.fieldMeta && 'values' in field.fieldMeta) {
|
|
return field.fieldMeta.values?.length === 0;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
const emptyCheckboxFields = useMemo(
|
|
() => filterFieldsWithEmptyValues(localFields, FieldType.CHECKBOX),
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[localFields],
|
|
);
|
|
|
|
const emptyRadioFields = useMemo(
|
|
() => filterFieldsWithEmptyValues(localFields, FieldType.RADIO),
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[localFields],
|
|
);
|
|
|
|
const emptySelectFields = useMemo(
|
|
() => filterFieldsWithEmptyValues(localFields, FieldType.DROPDOWN),
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[localFields],
|
|
);
|
|
|
|
const hasErrors =
|
|
emptyCheckboxFields.length > 0 || emptyRadioFields.length > 0 || emptySelectFields.length > 0;
|
|
|
|
const [isFieldWithinBounds, setIsFieldWithinBounds] = useState(false);
|
|
const [coords, setCoords] = useState({
|
|
x: 0,
|
|
y: 0,
|
|
});
|
|
|
|
const fieldBounds = useRef({
|
|
height: 0,
|
|
width: 0,
|
|
});
|
|
|
|
const onMouseMove = useCallback(
|
|
(event: MouseEvent) => {
|
|
setIsFieldWithinBounds(
|
|
isWithinPageBounds(
|
|
event,
|
|
PDF_VIEWER_PAGE_SELECTOR,
|
|
fieldBounds.current.width,
|
|
fieldBounds.current.height,
|
|
),
|
|
);
|
|
|
|
setCoords({
|
|
x: event.clientX - fieldBounds.current.width / 2,
|
|
y: event.clientY - fieldBounds.current.height / 2,
|
|
});
|
|
},
|
|
[isWithinPageBounds],
|
|
);
|
|
|
|
const onMouseClick = useCallback(
|
|
(event: MouseEvent) => {
|
|
if (!selectedField || !selectedSigner) {
|
|
return;
|
|
}
|
|
|
|
const $page = getPage(event, PDF_VIEWER_PAGE_SELECTOR);
|
|
|
|
if (
|
|
!$page ||
|
|
!isWithinPageBounds(
|
|
event,
|
|
PDF_VIEWER_PAGE_SELECTOR,
|
|
fieldBounds.current.width,
|
|
fieldBounds.current.height,
|
|
)
|
|
) {
|
|
setSelectedField(null);
|
|
return;
|
|
}
|
|
|
|
const { top, left, height, width } = getBoundingClientRect($page);
|
|
|
|
const pageNumber = parseInt($page.getAttribute('data-page-number') ?? '1', 10);
|
|
|
|
// Calculate x and y as a percentage of the page width and height
|
|
let pageX = ((event.pageX - left) / width) * 100;
|
|
let pageY = ((event.pageY - top) / height) * 100;
|
|
|
|
// Get the bounds as a percentage of the page width and height
|
|
const fieldPageWidth = (fieldBounds.current.width / width) * 100;
|
|
const fieldPageHeight = (fieldBounds.current.height / height) * 100;
|
|
|
|
// And center it based on the bounds
|
|
pageX -= fieldPageWidth / 2;
|
|
pageY -= fieldPageHeight / 2;
|
|
|
|
append({
|
|
formId: nanoid(12),
|
|
type: selectedField,
|
|
pageNumber,
|
|
pageX,
|
|
pageY,
|
|
pageWidth: fieldPageWidth,
|
|
pageHeight: fieldPageHeight,
|
|
signerEmail: selectedSigner.email,
|
|
signerId: selectedSigner.id,
|
|
signerToken: selectedSigner.token ?? '',
|
|
fieldMeta: undefined,
|
|
});
|
|
|
|
setIsFieldWithinBounds(false);
|
|
setSelectedField(null);
|
|
},
|
|
[append, isWithinPageBounds, selectedField, selectedSigner, getPage],
|
|
);
|
|
|
|
const onFieldResize = useCallback(
|
|
(node: HTMLElement, index: number) => {
|
|
const field = localFields[index];
|
|
|
|
const $page = window.document.querySelector<HTMLElement>(
|
|
`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`,
|
|
);
|
|
|
|
if (!$page) {
|
|
return;
|
|
}
|
|
|
|
const {
|
|
x: pageX,
|
|
y: pageY,
|
|
width: pageWidth,
|
|
height: pageHeight,
|
|
} = getFieldPosition($page, node);
|
|
|
|
update(index, {
|
|
...field,
|
|
pageX,
|
|
pageY,
|
|
pageWidth,
|
|
pageHeight,
|
|
});
|
|
},
|
|
[getFieldPosition, localFields, update],
|
|
);
|
|
|
|
const onFieldMove = useCallback(
|
|
(node: HTMLElement, index: number) => {
|
|
const field = localFields[index];
|
|
|
|
const $page = window.document.querySelector<HTMLElement>(
|
|
`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`,
|
|
);
|
|
|
|
if (!$page) {
|
|
return;
|
|
}
|
|
|
|
const { x: pageX, y: pageY } = getFieldPosition($page, node);
|
|
|
|
update(index, {
|
|
...field,
|
|
pageX,
|
|
pageY,
|
|
});
|
|
},
|
|
[getFieldPosition, localFields, update],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (selectedField) {
|
|
window.addEventListener('mousemove', onMouseMove);
|
|
window.addEventListener('mouseup', onMouseClick);
|
|
}
|
|
|
|
return () => {
|
|
window.removeEventListener('mousemove', onMouseMove);
|
|
window.removeEventListener('mouseup', onMouseClick);
|
|
};
|
|
}, [onMouseClick, onMouseMove, selectedField]);
|
|
|
|
useEffect(() => {
|
|
const observer = new MutationObserver((_mutations) => {
|
|
const $page = document.querySelector(PDF_VIEWER_PAGE_SELECTOR);
|
|
|
|
if (!$page) {
|
|
return;
|
|
}
|
|
|
|
fieldBounds.current = {
|
|
height: Math.max(DEFAULT_HEIGHT_PX),
|
|
width: Math.max(DEFAULT_WIDTH_PX),
|
|
};
|
|
});
|
|
|
|
observer.observe(document.body, {
|
|
childList: true,
|
|
subtree: true,
|
|
});
|
|
|
|
return () => {
|
|
observer.disconnect();
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
setSelectedSigner(recipients[0]);
|
|
}, [recipients]);
|
|
|
|
const recipientsByRole = useMemo(() => {
|
|
const recipientsByRole: Record<RecipientRole, Recipient[]> = {
|
|
CC: [],
|
|
VIEWER: [],
|
|
SIGNER: [],
|
|
APPROVER: [],
|
|
ASSISTANT: [],
|
|
};
|
|
|
|
recipients.forEach((recipient) => {
|
|
recipientsByRole[recipient.role].push(recipient);
|
|
});
|
|
|
|
return recipientsByRole;
|
|
}, [recipients]);
|
|
|
|
useEffect(() => {
|
|
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 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 &&
|
|
role !== RecipientRole.ASSISTANT,
|
|
);
|
|
}, [recipientsByRole]);
|
|
|
|
const handleAdvancedSettings = () => {
|
|
setShowAdvancedSettings((prev) => !prev);
|
|
};
|
|
|
|
const handleSavedFieldSettings = (fieldState: FieldMeta) => {
|
|
const initialValues = form.getValues();
|
|
|
|
const updatedFields = initialValues.fields.map((field) => {
|
|
if (field.formId === currentField?.formId) {
|
|
const parsedFieldMeta = ZFieldMetaSchema.parse(fieldState);
|
|
|
|
return {
|
|
...field,
|
|
fieldMeta: parsedFieldMeta,
|
|
};
|
|
}
|
|
|
|
return field;
|
|
});
|
|
|
|
form.setValue('fields', updatedFields);
|
|
};
|
|
|
|
const isTypedSignatureEnabled = form.watch('typedSignatureEnabled');
|
|
|
|
const handleTypedSignatureChange = (value: boolean) => {
|
|
form.setValue('typedSignatureEnabled', value, { shouldDirty: true });
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{showAdvancedSettings && currentField ? (
|
|
<FieldAdvancedSettings
|
|
title={msg`Advanced settings`}
|
|
description={msg`Configure the ${parseMessageDescriptor(
|
|
_,
|
|
FRIENDLY_FIELD_TYPE[currentField.type],
|
|
)} field`}
|
|
field={currentField}
|
|
fields={localFields}
|
|
onAdvancedSettings={handleAdvancedSettings}
|
|
onSave={handleSavedFieldSettings}
|
|
teamId={teamId}
|
|
/>
|
|
) : (
|
|
<>
|
|
<DocumentFlowFormContainerHeader
|
|
title={documentFlow.title}
|
|
description={documentFlow.description}
|
|
/>
|
|
<DocumentFlowFormContainerContent>
|
|
<div className="flex flex-col">
|
|
{selectedField && (
|
|
<div
|
|
className={cn(
|
|
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center bg-white transition duration-200 [container-type:size]',
|
|
selectedSignerStyles.default.base,
|
|
{
|
|
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
|
|
'dark:text-black/60': isFieldWithinBounds,
|
|
},
|
|
)}
|
|
style={{
|
|
top: coords.y,
|
|
left: coords.x,
|
|
height: fieldBounds.current.height,
|
|
width: fieldBounds.current.width,
|
|
}}
|
|
>
|
|
<span className="text-[clamp(0.425rem,25cqw,0.825rem)]">
|
|
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[selectedField])}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{localFields.map((field, index) => {
|
|
const recipientIndex = recipients.findIndex((r) => r.email === field.signerEmail);
|
|
|
|
return (
|
|
<FieldItem
|
|
key={index}
|
|
recipientIndex={recipientIndex === -1 ? 0 : recipientIndex}
|
|
field={field}
|
|
disabled={selectedSigner?.email !== field.signerEmail}
|
|
minHeight={MIN_HEIGHT_PX}
|
|
minWidth={MIN_WIDTH_PX}
|
|
defaultHeight={DEFAULT_HEIGHT_PX}
|
|
defaultWidth={DEFAULT_WIDTH_PX}
|
|
passive={isFieldWithinBounds && !!selectedField}
|
|
onFocus={() => setLastActiveField(field)}
|
|
onBlur={() => setLastActiveField(null)}
|
|
onResize={(options) => onFieldResize(options, index)}
|
|
onMove={(options) => onFieldMove(options, index)}
|
|
onRemove={() => remove(index)}
|
|
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
|
|
onAdvancedSettings={() => {
|
|
setCurrentField(field);
|
|
handleAdvancedSettings();
|
|
}}
|
|
hideRecipients={hideRecipients}
|
|
active={activeFieldId === field.formId}
|
|
onFieldActivate={() => setActiveFieldId(field.formId)}
|
|
onFieldDeactivate={() => setActiveFieldId(null)}
|
|
/>
|
|
);
|
|
})}
|
|
|
|
{!hideRecipients && (
|
|
<Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
role="combobox"
|
|
className={cn(
|
|
'bg-background text-muted-foreground hover:text-foreground mb-12 mt-2 justify-between font-normal',
|
|
selectedSignerStyles.default.base,
|
|
)}
|
|
>
|
|
{selectedSigner?.email && (
|
|
<span className="flex-1 truncate text-left">
|
|
{selectedSigner?.name} ({selectedSigner?.email})
|
|
</span>
|
|
)}
|
|
|
|
{!selectedSigner?.email && (
|
|
<span className="gradie flex-1 truncate text-left">
|
|
{selectedSigner?.email}
|
|
</span>
|
|
)}
|
|
|
|
<ChevronsUpDown className="ml-2 h-4 w-4" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
|
|
<PopoverContent className="p-0" align="start">
|
|
<Command value={selectedSigner?.email}>
|
|
<CommandInput />
|
|
|
|
<CommandEmpty>
|
|
<span className="text-muted-foreground inline-block px-4">
|
|
<Trans>No recipient matching this description was found.</Trans>
|
|
</span>
|
|
</CommandEmpty>
|
|
|
|
{recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => (
|
|
<CommandGroup key={roleIndex}>
|
|
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
|
|
{_(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)}
|
|
</div>
|
|
|
|
{roleRecipients.length === 0 && (
|
|
<div
|
|
key={`${role}-empty`}
|
|
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs"
|
|
>
|
|
<Trans>No recipients with this role</Trans>
|
|
</div>
|
|
)}
|
|
|
|
{roleRecipients.map((recipient) => (
|
|
<CommandItem
|
|
key={recipient.id}
|
|
className={cn(
|
|
'px-2 last:mb-1 [&:not(:first-child)]:mt-1',
|
|
getSignerColorStyles(
|
|
Math.max(
|
|
recipients.findIndex((r) => r.id === recipient.id),
|
|
0,
|
|
),
|
|
).default.comboxBoxItem,
|
|
)}
|
|
onSelect={() => {
|
|
setSelectedSigner(recipient);
|
|
setShowRecipientsSelector(false);
|
|
}}
|
|
>
|
|
<span
|
|
className={cn('text-foreground/70 truncate', {
|
|
'text-foreground/80': recipient === selectedSigner,
|
|
})}
|
|
>
|
|
{recipient.name && (
|
|
<span title={`${recipient.name} (${recipient.email})`}>
|
|
{recipient.name} ({recipient.email})
|
|
</span>
|
|
)}
|
|
|
|
{!recipient.name && (
|
|
<span title={recipient.email}>{recipient.email}</span>
|
|
)}
|
|
</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
))}
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)}
|
|
|
|
<Form {...form}>
|
|
<FormField
|
|
control={form.control}
|
|
name="typedSignatureEnabled"
|
|
render={({ field: { value, ...field } }) => (
|
|
<FormItem className="mb-6 flex flex-row items-center space-x-2 space-y-0">
|
|
<FormControl>
|
|
<Checkbox
|
|
{...field}
|
|
id="typedSignatureEnabled"
|
|
checked={value}
|
|
onCheckedChange={(checked) => field.onChange(checked)}
|
|
disabled={form.formState.isSubmitting}
|
|
/>
|
|
</FormControl>
|
|
|
|
<FormLabel
|
|
htmlFor="typedSignatureEnabled"
|
|
className="text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
>
|
|
<Trans>Enable Typed Signatures</Trans>
|
|
</FormLabel>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<div className="-mx-2 flex-1 overflow-y-auto px-2">
|
|
<fieldset className="my-2 grid grid-cols-3 gap-4">
|
|
<button
|
|
type="button"
|
|
className="group h-full w-full"
|
|
onClick={() => setSelectedField(FieldType.SIGNATURE)}
|
|
onMouseDown={() => setSelectedField(FieldType.SIGNATURE)}
|
|
data-selected={selectedField === FieldType.SIGNATURE ? true : undefined}
|
|
>
|
|
<Card
|
|
className={cn(
|
|
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
|
// selectedSignerStyles.borderClass,
|
|
)}
|
|
>
|
|
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
|
<p
|
|
className={cn(
|
|
'text-muted-foreground group-data-[selected]:text-foreground font-signature flex items-center justify-center gap-x-1.5 text-lg font-normal',
|
|
)}
|
|
>
|
|
<Trans>Signature</Trans>
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
className="group h-full w-full"
|
|
onClick={() => setSelectedField(FieldType.INITIALS)}
|
|
onMouseDown={() => setSelectedField(FieldType.INITIALS)}
|
|
data-selected={selectedField === FieldType.INITIALS ? true : undefined}
|
|
>
|
|
<Card
|
|
className={cn(
|
|
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
|
// selectedSignerStyles.borderClass,
|
|
)}
|
|
>
|
|
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
|
<p
|
|
className={cn(
|
|
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
|
)}
|
|
>
|
|
<Contact className="h-4 w-4" />
|
|
Initials
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
className="group h-full w-full"
|
|
onClick={() => setSelectedField(FieldType.EMAIL)}
|
|
onMouseDown={() => setSelectedField(FieldType.EMAIL)}
|
|
data-selected={selectedField === FieldType.EMAIL ? true : undefined}
|
|
>
|
|
<Card
|
|
className={cn(
|
|
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
|
// selectedSignerStyles.borderClass,
|
|
)}
|
|
>
|
|
<CardContent className="p-4">
|
|
<p
|
|
className={cn(
|
|
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
|
)}
|
|
>
|
|
<Mail className="h-4 w-4" />
|
|
<Trans>Email</Trans>
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
className="group h-full w-full"
|
|
onClick={() => setSelectedField(FieldType.NAME)}
|
|
onMouseDown={() => setSelectedField(FieldType.NAME)}
|
|
data-selected={selectedField === FieldType.NAME ? true : undefined}
|
|
>
|
|
<Card
|
|
className={cn(
|
|
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
|
// selectedSignerStyles.borderClass,
|
|
)}
|
|
>
|
|
<CardContent className="p-4">
|
|
<p
|
|
className={cn(
|
|
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
|
)}
|
|
>
|
|
<User className="h-4 w-4" />
|
|
<Trans>Name</Trans>
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
className="group h-full w-full"
|
|
onClick={() => setSelectedField(FieldType.DATE)}
|
|
onMouseDown={() => setSelectedField(FieldType.DATE)}
|
|
data-selected={selectedField === FieldType.DATE ? true : undefined}
|
|
>
|
|
<Card
|
|
className={cn(
|
|
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
|
// selectedSignerStyles.borderClass,
|
|
)}
|
|
>
|
|
<CardContent className="p-4">
|
|
<p
|
|
className={cn(
|
|
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
|
)}
|
|
>
|
|
<CalendarDays className="h-4 w-4" />
|
|
<Trans>Date</Trans>
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
className="group h-full w-full"
|
|
onClick={() => setSelectedField(FieldType.TEXT)}
|
|
onMouseDown={() => setSelectedField(FieldType.TEXT)}
|
|
data-selected={selectedField === FieldType.TEXT ? true : undefined}
|
|
>
|
|
<Card
|
|
className={cn(
|
|
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
|
// selectedSignerStyles.borderClass,
|
|
)}
|
|
>
|
|
<CardContent className="p-4">
|
|
<p
|
|
className={cn(
|
|
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
|
)}
|
|
>
|
|
<Type className="h-4 w-4" />
|
|
<Trans>Text</Trans>
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
className="group h-full w-full"
|
|
onClick={() => setSelectedField(FieldType.NUMBER)}
|
|
onMouseDown={() => setSelectedField(FieldType.NUMBER)}
|
|
data-selected={selectedField === FieldType.NUMBER ? true : undefined}
|
|
>
|
|
<Card
|
|
className={cn(
|
|
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
|
// selectedSignerStyles.borderClass,
|
|
)}
|
|
>
|
|
<CardContent className="p-4">
|
|
<p
|
|
className={cn(
|
|
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
|
)}
|
|
>
|
|
<Hash className="h-4 w-4" />
|
|
<Trans>Number</Trans>
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
className="group h-full w-full"
|
|
onClick={() => setSelectedField(FieldType.RADIO)}
|
|
onMouseDown={() => setSelectedField(FieldType.RADIO)}
|
|
data-selected={selectedField === FieldType.RADIO ? true : undefined}
|
|
>
|
|
<Card
|
|
className={cn(
|
|
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
|
// selectedSignerStyles.borderClass,
|
|
)}
|
|
>
|
|
<CardContent className="p-4">
|
|
<p
|
|
className={cn(
|
|
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
|
)}
|
|
>
|
|
<Disc className="h-4 w-4" />
|
|
Radio
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
className="group h-full w-full"
|
|
onClick={() => setSelectedField(FieldType.CHECKBOX)}
|
|
onMouseDown={() => setSelectedField(FieldType.CHECKBOX)}
|
|
data-selected={selectedField === FieldType.CHECKBOX ? true : undefined}
|
|
>
|
|
<Card
|
|
className={cn(
|
|
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
|
// selectedSignerStyles.borderClass,
|
|
)}
|
|
>
|
|
<CardContent className="p-4">
|
|
<p
|
|
className={cn(
|
|
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
|
)}
|
|
>
|
|
<CheckSquare className="h-4 w-4" />
|
|
{/* Not translated on purpose. */}
|
|
Checkbox
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
className="group h-full w-full"
|
|
onClick={() => setSelectedField(FieldType.DROPDOWN)}
|
|
onMouseDown={() => setSelectedField(FieldType.DROPDOWN)}
|
|
data-selected={selectedField === FieldType.DROPDOWN ? true : undefined}
|
|
>
|
|
<Card
|
|
className={cn(
|
|
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
|
// selectedSignerStyles.borderClass,
|
|
)}
|
|
>
|
|
<CardContent className="p-4">
|
|
<p
|
|
className={cn(
|
|
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
|
|
)}
|
|
>
|
|
<ChevronDown className="h-4 w-4" />
|
|
<Trans>Dropdown</Trans>
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</button>
|
|
</fieldset>
|
|
</div>
|
|
</Form>
|
|
|
|
{hasErrors && (
|
|
<div className="mt-4">
|
|
<ul>
|
|
<li className="text-sm text-red-500">
|
|
<Trans>
|
|
To proceed further, please set at least one value for the{' '}
|
|
{emptyCheckboxFields.length > 0
|
|
? 'Checkbox'
|
|
: emptyRadioFields.length > 0
|
|
? 'Radio'
|
|
: 'Select'}{' '}
|
|
field.
|
|
</Trans>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
<DocumentFlowFormContainerFooter>
|
|
<DocumentFlowFormContainerStep step={currentStep} maxStep={totalSteps} />
|
|
|
|
<DocumentFlowFormContainerActions
|
|
loading={form.formState.isSubmitting}
|
|
disabled={form.formState.isSubmitting}
|
|
goNextLabel={msg`Save Template`}
|
|
disableNextStep={hasErrors}
|
|
onGoBackClick={() => {
|
|
previousStep();
|
|
remove();
|
|
}}
|
|
onGoNextClick={() => void onFormSubmit()}
|
|
/>
|
|
</DocumentFlowFormContainerFooter>
|
|
</div>
|
|
</DocumentFlowFormContainerContent>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
};
|