mirror of
https://github.com/documenso/documenso.git
synced 2025-11-20 03:32:14 +10:00
Merge branch 'main' into feat/signing-reminders
This commit is contained in:
@ -3,9 +3,8 @@ 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 { Prisma } from '@prisma/client';
|
||||
import type { Field, Recipient } from '@prisma/client';
|
||||
import { FieldType, RecipientRole, SendStatus } from '@prisma/client';
|
||||
import { FieldType, Prisma, RecipientRole, SendStatus } from '@prisma/client';
|
||||
import {
|
||||
CalendarDays,
|
||||
CheckSquare,
|
||||
@ -38,7 +37,7 @@ import {
|
||||
} from '@documenso/lib/utils/recipients';
|
||||
|
||||
import { FieldToolTip } from '../../components/field/field-tooltip';
|
||||
import { useSignerColors } from '../../lib/signer-colors';
|
||||
import { useRecipientColors } from '../../lib/recipient-colors';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Alert, AlertDescription } from '../alert';
|
||||
import { Card, CardContent } from '../card';
|
||||
@ -86,7 +85,7 @@ export type AddFieldsFormProps = {
|
||||
onSubmit: (_data: TAddFieldsFormSchema) => void;
|
||||
canGoBack?: boolean;
|
||||
isDocumentPdfLoaded: boolean;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export const AddFieldsFormPartial = ({
|
||||
@ -167,7 +166,6 @@ export const AddFieldsFormPartial = ({
|
||||
|
||||
const [selectedField, setSelectedField] = useState<FieldType | null>(null);
|
||||
const [selectedSigner, setSelectedSigner] = useState<Recipient | null>(null);
|
||||
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
|
||||
const [lastActiveField, setLastActiveField] = useState<TAddFieldsFormSchema['fields'][0] | null>(
|
||||
null,
|
||||
);
|
||||
@ -175,9 +173,10 @@ export const AddFieldsFormPartial = ({
|
||||
null,
|
||||
);
|
||||
const selectedSignerIndex = recipients.findIndex((r) => r.id === selectedSigner?.id);
|
||||
const selectedSignerStyles = useSignerColors(
|
||||
const selectedSignerStyles = useRecipientColors(
|
||||
selectedSignerIndex === -1 ? 0 : selectedSignerIndex,
|
||||
);
|
||||
|
||||
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||
|
||||
const filterFieldsWithEmptyValues = (fields: typeof localFields, fieldType: string) =>
|
||||
@ -400,35 +399,60 @@ export const AddFieldsFormPartial = ({
|
||||
);
|
||||
|
||||
const onFieldCopy = useCallback(
|
||||
(event?: KeyboardEvent | null, options?: { duplicate?: boolean }) => {
|
||||
const { duplicate = false } = options ?? {};
|
||||
(event?: KeyboardEvent | null, options?: { duplicate?: boolean; duplicateAll?: boolean }) => {
|
||||
const { duplicate = false, duplicateAll = false } = options ?? {};
|
||||
|
||||
if (lastActiveField) {
|
||||
event?.preventDefault();
|
||||
|
||||
if (!duplicate) {
|
||||
setFieldClipboard(lastActiveField);
|
||||
if (duplicate) {
|
||||
const newField: TAddFieldsFormSchema['fields'][0] = {
|
||||
...structuredClone(lastActiveField),
|
||||
nativeId: undefined,
|
||||
formId: nanoid(12),
|
||||
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
|
||||
pageX: lastActiveField.pageX + 3,
|
||||
pageY: lastActiveField.pageY + 3,
|
||||
};
|
||||
|
||||
toast({
|
||||
title: 'Copied field',
|
||||
description: 'Copied field to clipboard',
|
||||
append(newField);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (duplicateAll) {
|
||||
const pages = Array.from(document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR));
|
||||
|
||||
pages.forEach((_, index) => {
|
||||
const pageNumber = index + 1;
|
||||
|
||||
if (pageNumber === lastActiveField.pageNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newField: TAddFieldsFormSchema['fields'][0] = {
|
||||
...structuredClone(lastActiveField),
|
||||
nativeId: undefined,
|
||||
formId: nanoid(12),
|
||||
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
|
||||
pageNumber,
|
||||
};
|
||||
|
||||
append(newField);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const newField: TAddFieldsFormSchema['fields'][0] = {
|
||||
...structuredClone(lastActiveField),
|
||||
formId: nanoid(12),
|
||||
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
|
||||
pageX: lastActiveField.pageX + 3,
|
||||
pageY: lastActiveField.pageY + 3,
|
||||
};
|
||||
setFieldClipboard(lastActiveField);
|
||||
|
||||
append(newField);
|
||||
toast({
|
||||
title: 'Copied field',
|
||||
description: 'Copied field to clipboard',
|
||||
});
|
||||
}
|
||||
},
|
||||
[append, lastActiveField, selectedSigner?.email, toast],
|
||||
[append, lastActiveField, selectedSigner?.email, selectedSigner?.id, toast],
|
||||
);
|
||||
|
||||
const onFieldPaste = useCallback(
|
||||
@ -440,6 +464,7 @@ export const AddFieldsFormPartial = ({
|
||||
|
||||
append({
|
||||
...copiedField,
|
||||
nativeId: undefined,
|
||||
formId: nanoid(12),
|
||||
signerEmail: selectedSigner?.email ?? copiedField.signerEmail,
|
||||
pageX: copiedField.pageX + 3,
|
||||
@ -579,7 +604,6 @@ export const AddFieldsFormPartial = ({
|
||||
onAdvancedSettings={handleAdvancedSettings}
|
||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
||||
onSave={handleSavedFieldSettings}
|
||||
teamId={teamId}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
@ -593,13 +617,12 @@ export const AddFieldsFormPartial = ({
|
||||
{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,
|
||||
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center rounded-[2px] bg-white ring-2 transition duration-200 [container-type:size]',
|
||||
selectedSignerStyles?.base,
|
||||
{
|
||||
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
|
||||
'dark:text-black/60': isFieldWithinBounds,
|
||||
},
|
||||
// selectedField === FieldType.SIGNATURE && fontCaveat.className,
|
||||
)}
|
||||
style={{
|
||||
top: coords.y,
|
||||
@ -638,15 +661,17 @@ export const AddFieldsFormPartial = ({
|
||||
passive={isFieldWithinBounds && !!selectedField}
|
||||
onFocus={() => setLastActiveField(field)}
|
||||
onBlur={() => setLastActiveField(null)}
|
||||
onMouseEnter={() => setLastActiveField(field)}
|
||||
onMouseLeave={() => setLastActiveField(null)}
|
||||
onResize={(options) => onFieldResize(options, index)}
|
||||
onMove={(options) => onFieldMove(options, index)}
|
||||
onRemove={() => remove(index)}
|
||||
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
|
||||
onDuplicateAllPages={() => onFieldCopy(null, { duplicateAll: true })}
|
||||
onAdvancedSettings={() => {
|
||||
setCurrentField(field);
|
||||
handleAdvancedSettings();
|
||||
}}
|
||||
hideRecipients={hideRecipients}
|
||||
hasErrors={!!hasFieldError}
|
||||
active={activeFieldId === field.formId}
|
||||
onFieldActivate={() => setActiveFieldId(field.formId)}
|
||||
@ -677,7 +702,6 @@ export const AddFieldsFormPartial = ({
|
||||
<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">
|
||||
@ -702,7 +726,6 @@ export const AddFieldsFormPartial = ({
|
||||
<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">
|
||||
@ -728,7 +751,6 @@ export const AddFieldsFormPartial = ({
|
||||
<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">
|
||||
@ -754,7 +776,6 @@ export const AddFieldsFormPartial = ({
|
||||
<Card
|
||||
className={cn(
|
||||
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
||||
// selectedSignerStyles.borderClass,
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
@ -780,7 +801,6 @@ export const AddFieldsFormPartial = ({
|
||||
<Card
|
||||
className={cn(
|
||||
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
||||
// selectedSignerStyles.borderClass,
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
@ -806,7 +826,6 @@ export const AddFieldsFormPartial = ({
|
||||
<Card
|
||||
className={cn(
|
||||
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
||||
// selectedSignerStyles.borderClass,
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
@ -832,7 +851,6 @@ export const AddFieldsFormPartial = ({
|
||||
<Card
|
||||
className={cn(
|
||||
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
||||
// selectedSignerStyles.borderClass,
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
@ -858,7 +876,6 @@ export const AddFieldsFormPartial = ({
|
||||
<Card
|
||||
className={cn(
|
||||
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
||||
// selectedSignerStyles.borderClass,
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
@ -884,7 +901,6 @@ export const AddFieldsFormPartial = ({
|
||||
<Card
|
||||
className={cn(
|
||||
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
||||
// selectedSignerStyles.borderClass,
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
@ -911,7 +927,6 @@ export const AddFieldsFormPartial = ({
|
||||
<Card
|
||||
className={cn(
|
||||
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
||||
// selectedSignerStyles.borderClass,
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
|
||||
@ -15,6 +15,7 @@ import { InfoIcon } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
import { DOCUMENT_SIGNATURE_TYPES } from '@documenso/lib/constants/document';
|
||||
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
||||
@ -30,6 +31,10 @@ import {
|
||||
DocumentGlobalAuthActionSelect,
|
||||
DocumentGlobalAuthActionTooltip,
|
||||
} from '@documenso/ui/components/document/document-global-auth-action-select';
|
||||
import {
|
||||
DocumentReadOnlyFields,
|
||||
mapFieldsWithRecipients,
|
||||
} from '@documenso/ui/components/document/document-read-only-fields';
|
||||
import {
|
||||
DocumentVisibilitySelect,
|
||||
DocumentVisibilityTooltip,
|
||||
@ -65,14 +70,12 @@ import {
|
||||
DocumentFlowFormContainerHeader,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
import { ShowFieldItem } from './show-field-item';
|
||||
import type { DocumentFlowStep } from './types';
|
||||
|
||||
export type AddSettingsFormProps = {
|
||||
documentFlow: DocumentFlowStep;
|
||||
recipients: Recipient[];
|
||||
fields: Field[];
|
||||
isDocumentEnterprise: boolean;
|
||||
isDocumentPdfLoaded: boolean;
|
||||
document: TDocument;
|
||||
currentTeamMemberRole?: TeamMemberRole;
|
||||
@ -83,7 +86,6 @@ export const AddSettingsFormPartial = ({
|
||||
documentFlow,
|
||||
recipients,
|
||||
fields,
|
||||
isDocumentEnterprise,
|
||||
isDocumentPdfLoaded,
|
||||
document,
|
||||
currentTeamMemberRole,
|
||||
@ -91,6 +93,8 @@ export const AddSettingsFormPartial = ({
|
||||
}: AddSettingsFormProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const organisation = useCurrentOrganisation();
|
||||
|
||||
const { documentAuthOption } = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
});
|
||||
@ -101,8 +105,8 @@ export const AddSettingsFormPartial = ({
|
||||
title: document.title,
|
||||
externalId: document.externalId || '',
|
||||
visibility: document.visibility || '',
|
||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
|
||||
globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
|
||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || [],
|
||||
globalActionAuth: documentAuthOption?.globalActionAuth || [],
|
||||
|
||||
meta: {
|
||||
timezone:
|
||||
@ -135,6 +139,12 @@ export const AddSettingsFormPartial = ({
|
||||
)
|
||||
.otherwise(() => false);
|
||||
|
||||
const onFormSubmit = form.handleSubmit(onSubmit);
|
||||
|
||||
const onGoNextClick = () => {
|
||||
void onFormSubmit().catch(console.error);
|
||||
};
|
||||
|
||||
// We almost always want to set the timezone to the user's local timezone to avoid confusion
|
||||
// when the document is signed.
|
||||
useEffect(() => {
|
||||
@ -161,10 +171,13 @@ export const AddSettingsFormPartial = ({
|
||||
/>
|
||||
|
||||
<DocumentFlowFormContainerContent>
|
||||
{isDocumentPdfLoaded &&
|
||||
fields.map((field, index) => (
|
||||
<ShowFieldItem key={index} field={field} recipients={recipients} />
|
||||
))}
|
||||
{isDocumentPdfLoaded && (
|
||||
<DocumentReadOnlyFields
|
||||
showRecipientColors={true}
|
||||
recipientIds={recipients.map((recipient) => recipient.id)}
|
||||
fields={mapFieldsWithRecipients(fields, recipients)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form {...form}>
|
||||
<fieldset
|
||||
@ -215,7 +228,11 @@ export const AddSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Select {...field} onValueChange={field.onChange}>
|
||||
<Select
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger className="bg-background">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@ -245,7 +262,11 @@ export const AddSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<DocumentGlobalAuthAccessSelect {...field} onValueChange={field.onChange} />
|
||||
<DocumentGlobalAuthAccessSelect
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
@ -275,7 +296,7 @@ export const AddSettingsFormPartial = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{isDocumentEnterprise && (
|
||||
{organisation.organisationClaim.flags.cfr21 && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="globalActionAuth"
|
||||
@ -287,7 +308,11 @@ export const AddSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<DocumentGlobalAuthActionSelect {...field} onValueChange={field.onChange} />
|
||||
<DocumentGlobalAuthActionSelect
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
@ -432,7 +457,7 @@ export const AddSettingsFormPartial = ({
|
||||
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
disabled={documentHasBeenSent}
|
||||
>
|
||||
@ -468,7 +493,7 @@ export const AddSettingsFormPartial = ({
|
||||
<Combobox
|
||||
className="bg-background"
|
||||
options={TIME_ZONES}
|
||||
{...field}
|
||||
value={field.value}
|
||||
onChange={(value) => value && field.onChange(value)}
|
||||
disabled={documentHasBeenSent}
|
||||
/>
|
||||
@ -523,7 +548,7 @@ export const AddSettingsFormPartial = ({
|
||||
disabled={form.formState.isSubmitting}
|
||||
canGoBack={stepIndex !== 0}
|
||||
onGoBackClick={previousStep}
|
||||
onGoNextClick={form.handleSubmit(onSubmit)}
|
||||
onGoNextClick={onGoNextClick}
|
||||
/>
|
||||
</DocumentFlowFormContainerFooter>
|
||||
</>
|
||||
|
||||
@ -16,17 +16,6 @@ import {
|
||||
ZDocumentMetaTimezoneSchema,
|
||||
} from '@documenso/trpc/server/document-router/schema';
|
||||
|
||||
export const ZMapNegativeOneToUndefinedSchema = z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => {
|
||||
if (val === '-1') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return val;
|
||||
});
|
||||
|
||||
export const ZAddSettingsFormSchema = z.object({
|
||||
title: z
|
||||
.string()
|
||||
@ -34,12 +23,12 @@ export const ZAddSettingsFormSchema = z.object({
|
||||
.min(1, { message: msg`Title cannot be empty`.id }),
|
||||
externalId: z.string().optional(),
|
||||
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
||||
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZDocumentAccessAuthTypesSchema.optional(),
|
||||
),
|
||||
globalActionAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZDocumentActionAuthTypesSchema.optional(),
|
||||
),
|
||||
globalAccessAuth: z
|
||||
.array(z.union([ZDocumentAccessAuthTypesSchema, z.literal('-1')]))
|
||||
.transform((val) => (val.length === 1 && val[0] === '-1' ? [] : val))
|
||||
.optional()
|
||||
.default([]),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema),
|
||||
meta: z.object({
|
||||
timezone: ZDocumentMetaTimezoneSchema.optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
||||
dateFormat: ZDocumentMetaDateFormatSchema.optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
||||
|
||||
@ -14,6 +14,7 @@ import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { prop, sortBy } from 'remeda';
|
||||
|
||||
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
@ -23,6 +24,10 @@ import { RecipientActionAuthSelect } from '@documenso/ui/components/recipient/re
|
||||
import { RecipientRoleSelect } from '@documenso/ui/components/recipient/recipient-role-select';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
import {
|
||||
DocumentReadOnlyFields,
|
||||
mapFieldsWithRecipients,
|
||||
} from '../../components/document/document-read-only-fields';
|
||||
import { Button } from '../button';
|
||||
import { Checkbox } from '../checkbox';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
|
||||
@ -40,7 +45,6 @@ import {
|
||||
DocumentFlowFormContainerHeader,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
import { ShowFieldItem } from './show-field-item';
|
||||
import { SigningOrderConfirmation } from './signing-order-confirmation';
|
||||
import type { DocumentFlowStep } from './types';
|
||||
|
||||
@ -50,7 +54,6 @@ export type AddSignersFormProps = {
|
||||
fields: Field[];
|
||||
signingOrder?: DocumentSigningOrder | null;
|
||||
allowDictateNextSigner?: boolean;
|
||||
isDocumentEnterprise: boolean;
|
||||
onSubmit: (_data: TAddSignersFormSchema) => void;
|
||||
isDocumentPdfLoaded: boolean;
|
||||
};
|
||||
@ -61,7 +64,6 @@ export const AddSignersFormPartial = ({
|
||||
fields,
|
||||
signingOrder,
|
||||
allowDictateNextSigner,
|
||||
isDocumentEnterprise,
|
||||
onSubmit,
|
||||
isDocumentPdfLoaded,
|
||||
}: AddSignersFormProps) => {
|
||||
@ -75,6 +77,8 @@ export const AddSignersFormPartial = ({
|
||||
|
||||
const { currentStep, totalSteps, previousStep } = useStep();
|
||||
|
||||
const organisation = useCurrentOrganisation();
|
||||
|
||||
const defaultRecipients = [
|
||||
{
|
||||
formId: initialId,
|
||||
@ -82,7 +86,7 @@ export const AddSignersFormPartial = ({
|
||||
email: '',
|
||||
role: RecipientRole.SIGNER,
|
||||
signingOrder: 1,
|
||||
actionAuth: undefined,
|
||||
actionAuth: [],
|
||||
},
|
||||
];
|
||||
|
||||
@ -116,10 +120,14 @@ export const AddSignersFormPartial = ({
|
||||
const recipientHasAuthOptions = recipients.find((recipient) => {
|
||||
const recipientAuthOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||
|
||||
return recipientAuthOptions?.accessAuth || recipientAuthOptions?.actionAuth;
|
||||
return (
|
||||
recipientAuthOptions.accessAuth.length > 0 || recipientAuthOptions.actionAuth.length > 0
|
||||
);
|
||||
});
|
||||
|
||||
const formHasActionAuth = form.getValues('signers').find((signer) => signer.actionAuth);
|
||||
const formHasActionAuth = form
|
||||
.getValues('signers')
|
||||
.find((signer) => signer.actionAuth.length > 0);
|
||||
|
||||
return recipientHasAuthOptions !== undefined || formHasActionAuth !== undefined;
|
||||
}, [recipients, form]);
|
||||
@ -187,7 +195,7 @@ export const AddSignersFormPartial = ({
|
||||
name: '',
|
||||
email: '',
|
||||
role: RecipientRole.SIGNER,
|
||||
actionAuth: undefined,
|
||||
actionAuth: [],
|
||||
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
||||
});
|
||||
};
|
||||
@ -223,7 +231,7 @@ export const AddSignersFormPartial = ({
|
||||
name: user?.name ?? '',
|
||||
email: user?.email ?? '',
|
||||
role: RecipientRole.SIGNER,
|
||||
actionAuth: undefined,
|
||||
actionAuth: [],
|
||||
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
||||
});
|
||||
}
|
||||
@ -368,10 +376,13 @@ export const AddSignersFormPartial = ({
|
||||
description={documentFlow.description}
|
||||
/>
|
||||
<DocumentFlowFormContainerContent>
|
||||
{isDocumentPdfLoaded &&
|
||||
fields.map((field, index) => (
|
||||
<ShowFieldItem key={index} field={field} recipients={recipients} />
|
||||
))}
|
||||
{isDocumentPdfLoaded && (
|
||||
<DocumentReadOnlyFields
|
||||
showRecipientColors={true}
|
||||
recipientIds={recipients.map((recipient) => recipient.id)}
|
||||
fields={mapFieldsWithRecipients(fields, recipients)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AnimateGenericFadeInOut motionKey={showAdvancedSettings ? 'Show' : 'Hide'}>
|
||||
<Form {...form}>
|
||||
@ -623,36 +634,37 @@ export const AddSignersFormPartial = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
{showAdvancedSettings && isDocumentEnterprise && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`signers.${index}.actionAuth`}
|
||||
render={({ field }) => (
|
||||
<FormItem
|
||||
className={cn('col-span-8', {
|
||||
'mb-6':
|
||||
form.formState.errors.signers?.[index] &&
|
||||
!form.formState.errors.signers[index]?.actionAuth,
|
||||
'col-span-10': isSigningOrderSequential,
|
||||
})}
|
||||
>
|
||||
<FormControl>
|
||||
<RecipientActionAuthSelect
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
disabled={
|
||||
snapshot.isDragging ||
|
||||
isSubmitting ||
|
||||
!canRecipientBeModified(signer.nativeId)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
{showAdvancedSettings &&
|
||||
organisation.organisationClaim.flags.cfr21 && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`signers.${index}.actionAuth`}
|
||||
render={({ field }) => (
|
||||
<FormItem
|
||||
className={cn('col-span-8', {
|
||||
'mb-6':
|
||||
form.formState.errors.signers?.[index] &&
|
||||
!form.formState.errors.signers[index]?.actionAuth,
|
||||
'col-span-10': isSigningOrderSequential,
|
||||
})}
|
||||
>
|
||||
<FormControl>
|
||||
<RecipientActionAuthSelect
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
disabled={
|
||||
snapshot.isDragging ||
|
||||
isSubmitting ||
|
||||
!canRecipientBeModified(signer.nativeId)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="col-span-2 flex gap-x-2">
|
||||
<FormField
|
||||
@ -750,7 +762,7 @@ export const AddSignersFormPartial = ({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{!alwaysShowAdvancedSettings && isDocumentEnterprise && (
|
||||
{!alwaysShowAdvancedSettings && organisation.organisationClaim.flags.cfr21 && (
|
||||
<div className="mt-4 flex flex-row items-center">
|
||||
<Checkbox
|
||||
id="showAdvancedRecipientSettings"
|
||||
|
||||
@ -4,8 +4,6 @@ import { z } from 'zod';
|
||||
|
||||
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
||||
|
||||
import { ZMapNegativeOneToUndefinedSchema } from './add-settings.types';
|
||||
|
||||
export const ZAddSignersFormSchema = z
|
||||
.object({
|
||||
signers: z.array(
|
||||
@ -19,9 +17,7 @@ export const ZAddSignersFormSchema = z
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
actionAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZRecipientActionAuthTypesSchema.optional(),
|
||||
),
|
||||
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||
}),
|
||||
),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||
|
||||
@ -5,23 +5,44 @@ import { Trans } from '@lingui/react/macro';
|
||||
import type { Field, Recipient } from '@prisma/client';
|
||||
import { DocumentDistributionMethod, DocumentStatus, RecipientRole } from '@prisma/client';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
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 { trpc } from '@documenso/trpc/react';
|
||||
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||
|
||||
import { CopyTextButton } from '../../components/common/copy-text-button';
|
||||
import { DocumentEmailCheckboxes } from '../../components/document/document-email-checkboxes';
|
||||
import {
|
||||
DocumentReadOnlyFields,
|
||||
mapFieldsWithRecipients,
|
||||
} from '../../components/document/document-read-only-fields';
|
||||
import { AvatarWithText } from '../avatar';
|
||||
import { FormErrorMessage } from '../form/form-error-message';
|
||||
import { Input } from '../input';
|
||||
import { Label } from '../label';
|
||||
import { useStep } from '../stepper';
|
||||
import { Textarea } from '../textarea';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
|
||||
import { toast } from '../use-toast';
|
||||
import { type TAddSubjectFormSchema, ZAddSubjectFormSchema } from './add-subject.types';
|
||||
import {
|
||||
@ -31,7 +52,6 @@ import {
|
||||
DocumentFlowFormContainerHeader,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
import { ShowFieldItem } from './show-field-item';
|
||||
import type { DocumentFlowStep } from './types';
|
||||
|
||||
export type AddSubjectFormProps = {
|
||||
@ -53,15 +73,14 @@ export const AddSubjectFormPartial = ({
|
||||
}: AddSubjectFormProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<TAddSubjectFormSchema>({
|
||||
const organisation = useCurrentOrganisation();
|
||||
|
||||
const form = useForm<TAddSubjectFormSchema>({
|
||||
defaultValues: {
|
||||
meta: {
|
||||
emailId: document.documentMeta?.emailId ?? null,
|
||||
emailReplyTo: document.documentMeta?.emailReplyTo || undefined,
|
||||
// emailReplyName: document.documentMeta?.emailReplyName || undefined,
|
||||
subject: document.documentMeta?.subject ?? '',
|
||||
message: document.documentMeta?.message ?? '',
|
||||
distributionMethod:
|
||||
@ -72,6 +91,21 @@ export const AddSubjectFormPartial = ({
|
||||
resolver: zodResolver(ZAddSubjectFormSchema),
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { isSubmitting },
|
||||
} = form;
|
||||
|
||||
const { data: emailData, isLoading: isLoadingEmails } =
|
||||
trpc.enterprise.organisation.email.find.useQuery({
|
||||
organisationId: organisation.id,
|
||||
perPage: 100,
|
||||
});
|
||||
|
||||
const emails = emailData?.data || [];
|
||||
|
||||
const GoNextLabel = {
|
||||
[DocumentDistributionMethod.EMAIL]: {
|
||||
[DocumentStatus.DRAFT]: msg`Send`,
|
||||
@ -103,10 +137,13 @@ export const AddSubjectFormPartial = ({
|
||||
/>
|
||||
<DocumentFlowFormContainerContent>
|
||||
<div className="flex flex-col">
|
||||
{isDocumentPdfLoaded &&
|
||||
fields.map((field, index) => (
|
||||
<ShowFieldItem key={index} field={field} recipients={recipients} />
|
||||
))}
|
||||
{isDocumentPdfLoaded && (
|
||||
<DocumentReadOnlyFields
|
||||
showRecipientColors={true}
|
||||
recipientIds={recipients.map((recipient) => recipient.id)}
|
||||
fields={mapFieldsWithRecipients(fields, recipients)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Tabs
|
||||
onValueChange={(value) =>
|
||||
@ -133,54 +170,141 @@ export const AddSubjectFormPartial = ({
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0, transition: { duration: 0.3 } }}
|
||||
exit={{ opacity: 0, transition: { duration: 0.15 } }}
|
||||
className="flex flex-col gap-y-4 rounded-lg border p-4"
|
||||
>
|
||||
<div>
|
||||
<Label htmlFor="subject">
|
||||
<Trans>
|
||||
Subject <span className="text-muted-foreground">(Optional)</span>
|
||||
</Trans>
|
||||
</Label>
|
||||
<Form {...form}>
|
||||
<fieldset
|
||||
className="flex flex-col gap-y-4 rounded-lg border p-4"
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
{organisation.organisationClaim.flags.emailDomains && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta.emailId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Email Sender</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<Input
|
||||
id="subject"
|
||||
className="bg-background mt-2"
|
||||
disabled={isSubmitting}
|
||||
{...register('meta.subject')}
|
||||
/>
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value === null ? '-1' : field.value}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value === '-1' ? null : value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger loading={isLoadingEmails} className="bg-background">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<FormErrorMessage className="mt-2" error={errors.meta?.subject} />
|
||||
</div>
|
||||
<SelectContent>
|
||||
{emails.map((email) => (
|
||||
<SelectItem key={email.id} value={email.id}>
|
||||
{email.email}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
<div>
|
||||
<Label htmlFor="message">
|
||||
<Trans>
|
||||
Message <span className="text-muted-foreground">(Optional)</span>
|
||||
</Trans>
|
||||
</Label>
|
||||
<SelectItem value={'-1'}>Documenso</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Textarea
|
||||
id="message"
|
||||
className="bg-background mt-2 h-32 resize-none"
|
||||
disabled={isSubmitting}
|
||||
{...register('meta.message')}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormErrorMessage
|
||||
className="mt-2"
|
||||
error={
|
||||
typeof errors.meta?.message !== 'string' ? errors.meta?.message : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta.emailReplyTo"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Reply To Email</Trans>{' '}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
</FormLabel>
|
||||
|
||||
<DocumentSendEmailMessageHelper />
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
|
||||
<DocumentEmailCheckboxes
|
||||
className="mt-2"
|
||||
value={emailSettings}
|
||||
onChange={(value) => setValue('meta.emailSettings', value)}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="meta.emailReplyName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Reply To Name</Trans>{' '}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/> */}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta.subject"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Subject</Trans>{' '}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta.message"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex flex-row items-center">
|
||||
<Trans>Message</Trans>{' '}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-muted-foreground p-4">
|
||||
<DocumentSendEmailMessageHelper />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Textarea className="bg-background mt-2 h-16 resize-none" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DocumentEmailCheckboxes
|
||||
className="mt-2"
|
||||
value={emailSettings}
|
||||
onChange={(value) => setValue('meta.emailSettings', value)}
|
||||
/>
|
||||
</fieldset>
|
||||
</Form>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@ -200,8 +324,8 @@ export const AddSubjectFormPartial = ({
|
||||
|
||||
<p className="mt-2">
|
||||
<Trans>
|
||||
We will generate signing links for with you, which you can send to the
|
||||
recipients through your method of choice.
|
||||
We will generate signing links for you, which you can send to the recipients
|
||||
through your method of choice.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -5,6 +5,12 @@ import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-emai
|
||||
|
||||
export const ZAddSubjectFormSchema = z.object({
|
||||
meta: z.object({
|
||||
emailId: z.string().nullable(),
|
||||
emailReplyTo: z.preprocess(
|
||||
(val) => (val === '' ? undefined : val),
|
||||
z.string().email().optional(),
|
||||
),
|
||||
// emailReplyName: z.string().optional(),
|
||||
subject: z.string(),
|
||||
message: z.string(),
|
||||
distributionMethod: z
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
import { ZCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import type { TCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
|
||||
import { FieldIcon } from '../field-icon';
|
||||
import type { TDocumentFlowFormSchema } from '../types';
|
||||
|
||||
type Field = TDocumentFlowFormSchema['fields'][0];
|
||||
|
||||
export type CheckboxFieldProps = {
|
||||
field: Field;
|
||||
};
|
||||
|
||||
export const CheckboxField = ({ field }: CheckboxFieldProps) => {
|
||||
let parsedFieldMeta: TCheckboxFieldMeta | undefined = undefined;
|
||||
|
||||
if (field.fieldMeta) {
|
||||
parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
||||
}
|
||||
|
||||
if (parsedFieldMeta && (!parsedFieldMeta.values || parsedFieldMeta.values.length === 0)) {
|
||||
return <FieldIcon fieldMeta={field.fieldMeta} type={field.type} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1">
|
||||
{!parsedFieldMeta?.values ? (
|
||||
<FieldIcon fieldMeta={field.fieldMeta} type={field.type} />
|
||||
) : (
|
||||
parsedFieldMeta.values.map((item: { value: string; checked: boolean }, index: number) => (
|
||||
<div key={index} className="flex items-center gap-x-1.5">
|
||||
<Checkbox
|
||||
className="dark:border-field-border h-3 w-3 bg-white"
|
||||
id={`checkbox-${index}`}
|
||||
checked={item.checked}
|
||||
/>
|
||||
<Label htmlFor={`checkbox-${index}`} className="text-xs font-normal text-black">
|
||||
{item.value}
|
||||
</Label>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,49 +0,0 @@
|
||||
import { ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import type { TRadioFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
|
||||
|
||||
import { FieldIcon } from '../field-icon';
|
||||
import type { TDocumentFlowFormSchema } from '../types';
|
||||
|
||||
type Field = TDocumentFlowFormSchema['fields'][0];
|
||||
|
||||
export type RadioFieldProps = {
|
||||
field: Field;
|
||||
};
|
||||
|
||||
export const RadioField = ({ field }: RadioFieldProps) => {
|
||||
let parsedFieldMeta: TRadioFieldMeta | undefined = undefined;
|
||||
|
||||
if (field.fieldMeta) {
|
||||
parsedFieldMeta = ZRadioFieldMeta.parse(field.fieldMeta);
|
||||
}
|
||||
|
||||
if (parsedFieldMeta && (!parsedFieldMeta.values || parsedFieldMeta.values.length === 0)) {
|
||||
return <FieldIcon fieldMeta={field.fieldMeta} type={field.type} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{!parsedFieldMeta?.values ? (
|
||||
<FieldIcon fieldMeta={field.fieldMeta} type={field.type} />
|
||||
) : (
|
||||
<RadioGroup className="gap-y-1">
|
||||
{parsedFieldMeta.values?.map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-x-1.5">
|
||||
<RadioGroupItem
|
||||
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 font-normal text-black">
|
||||
{item.value}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
208
packages/ui/primitives/document-flow/field-content.tsx
Normal file
208
packages/ui/primitives/document-flow/field-content.tsx
Normal file
@ -0,0 +1,208 @@
|
||||
import { useLingui } from '@lingui/react';
|
||||
import type { DocumentMeta, Signature, TemplateMeta } from '@prisma/client';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
import {
|
||||
DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||
convertToLocalSystemFormat,
|
||||
} from '@documenso/lib/constants/date-formats';
|
||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { fromCheckboxValue } from '@documenso/lib/universal/field-checkbox';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Checkbox } from '../checkbox';
|
||||
import { Label } from '../label';
|
||||
import { RadioGroup, RadioGroupItem } from '../radio-group';
|
||||
import { FRIENDLY_FIELD_TYPE } from './types';
|
||||
|
||||
type FieldIconProps = {
|
||||
/**
|
||||
* Loose field type since this is used for partial fields.
|
||||
*/
|
||||
field: {
|
||||
inserted?: boolean;
|
||||
customText?: string;
|
||||
type: FieldType;
|
||||
fieldMeta?: TFieldMetaSchema | null;
|
||||
signature?: Signature | null;
|
||||
};
|
||||
documentMeta?: Pick<DocumentMeta | TemplateMeta, 'dateFormat'>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the content inside field containers prior to sealing.
|
||||
*/
|
||||
export const FieldContent = ({ field, documentMeta }: FieldIconProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const { type, fieldMeta } = field;
|
||||
|
||||
// Render checkbox layout for checkbox fields, even if no values exist yet
|
||||
if (field.type === FieldType.CHECKBOX && field.fieldMeta?.type === 'checkbox') {
|
||||
let checkedValues: string[] = [];
|
||||
|
||||
try {
|
||||
checkedValues = fromCheckboxValue(field.customText ?? '');
|
||||
} catch (err) {
|
||||
// Do nothing.
|
||||
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
// If no values exist yet, show a placeholder checkbox
|
||||
if (!field.fieldMeta.values || field.fieldMeta.values.length === 0) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex gap-1 py-0.5',
|
||||
field.fieldMeta.direction === 'horizontal' ? 'flex-row flex-wrap' : 'flex-col gap-y-1',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Checkbox className="h-3 w-3" disabled />
|
||||
<Label className="text-foreground ml-1.5 text-xs font-normal opacity-50">
|
||||
Checkbox option
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex gap-1 py-0.5',
|
||||
field.fieldMeta.direction === 'horizontal' ? 'flex-row flex-wrap' : 'flex-col gap-y-1',
|
||||
)}
|
||||
>
|
||||
{field.fieldMeta.values.map((item, index) => (
|
||||
<div key={index} className="flex items-center">
|
||||
<Checkbox
|
||||
className="h-3 w-3"
|
||||
id={`checkbox-${index}`}
|
||||
checked={checkedValues.includes(
|
||||
item.value === '' ? `empty-value-${index + 1}` : item.value, // I got no idea...
|
||||
)}
|
||||
/>
|
||||
|
||||
{item.value && (
|
||||
<Label
|
||||
htmlFor={`checkbox-${index}`}
|
||||
className="text-foreground ml-1.5 text-xs font-normal"
|
||||
>
|
||||
{item.value}
|
||||
</Label>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Only render radio if values exist, otherwise render the empty radio field content.
|
||||
if (
|
||||
field.type === FieldType.RADIO &&
|
||||
field.fieldMeta?.type === 'radio' &&
|
||||
field.fieldMeta.values &&
|
||||
field.fieldMeta.values.length > 0
|
||||
) {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2 py-0.5">
|
||||
<RadioGroup className="gap-y-1">
|
||||
{field.fieldMeta.values.map((item, index) => (
|
||||
<div key={index} className="flex items-center">
|
||||
<RadioGroupItem
|
||||
className="pointer-events-none h-3 w-3"
|
||||
value={item.value}
|
||||
id={`option-${index}`}
|
||||
checked={item.value === field.customText}
|
||||
/>
|
||||
{item.value && (
|
||||
<Label
|
||||
htmlFor={`option-${index}`}
|
||||
className="text-foreground ml-1.5 text-xs font-normal"
|
||||
>
|
||||
{item.value}
|
||||
</Label>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
field.type === FieldType.DROPDOWN &&
|
||||
field.fieldMeta?.type === 'dropdown' &&
|
||||
!field.inserted
|
||||
) {
|
||||
return (
|
||||
<div className="text-field-card-foreground flex flex-row items-center py-0.5 text-[clamp(0.07rem,25cqw,0.825rem)] text-sm">
|
||||
<p>Select</p>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
field.type === FieldType.SIGNATURE &&
|
||||
field.signature?.signatureImageAsBase64 &&
|
||||
field.inserted
|
||||
) {
|
||||
return (
|
||||
<img
|
||||
src={field.signature.signatureImageAsBase64}
|
||||
alt="Signature"
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const labelToDisplay = fieldMeta?.label || _(FRIENDLY_FIELD_TYPE[type]) || '';
|
||||
let textToDisplay: string | undefined;
|
||||
|
||||
const isSignatureField =
|
||||
field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE;
|
||||
|
||||
if (field.type === FieldType.TEXT && field.fieldMeta?.type === 'text' && field.fieldMeta?.text) {
|
||||
textToDisplay = field.fieldMeta.text;
|
||||
}
|
||||
|
||||
if (field.inserted) {
|
||||
if (field.customText) {
|
||||
textToDisplay = field.customText;
|
||||
}
|
||||
|
||||
if (field.type === FieldType.DATE) {
|
||||
textToDisplay = convertToLocalSystemFormat(
|
||||
field.customText ?? '',
|
||||
documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||
);
|
||||
}
|
||||
|
||||
if (isSignatureField && field.signature?.typedSignature) {
|
||||
textToDisplay = field.signature.typedSignature;
|
||||
}
|
||||
}
|
||||
|
||||
const textAlign = fieldMeta && 'textAlign' in fieldMeta ? fieldMeta.textAlign : 'left';
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center overflow-hidden">
|
||||
<p
|
||||
className={cn(
|
||||
'text-foreground w-full whitespace-pre-wrap text-left text-[clamp(0.07rem,25cqw,0.825rem)] duration-200',
|
||||
{
|
||||
'!text-center': textAlign === 'center' || !textToDisplay,
|
||||
'!text-right': textAlign === 'right',
|
||||
'font-signature text-[clamp(0.07rem,25cqw,1.125rem)]': isSignatureField,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{textToDisplay || labelToDisplay}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,72 +0,0 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import {
|
||||
CalendarDays,
|
||||
CheckSquare,
|
||||
ChevronDown,
|
||||
Contact,
|
||||
Disc,
|
||||
Hash,
|
||||
Mail,
|
||||
Type,
|
||||
User,
|
||||
} from 'lucide-react';
|
||||
|
||||
import type { TFieldMetaSchema as FieldMetaType } from '@documenso/lib/types/field-meta';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
type FieldIconProps = {
|
||||
fieldMeta: FieldMetaType;
|
||||
type: FieldType;
|
||||
};
|
||||
|
||||
const fieldIcons = {
|
||||
[FieldType.INITIALS]: { icon: Contact, label: 'Initials' },
|
||||
[FieldType.EMAIL]: { icon: Mail, label: 'Email' },
|
||||
[FieldType.NAME]: { icon: User, label: 'Name' },
|
||||
[FieldType.DATE]: { icon: CalendarDays, label: 'Date' },
|
||||
[FieldType.TEXT]: { icon: Type, label: 'Text' },
|
||||
[FieldType.NUMBER]: { icon: Hash, label: 'Number' },
|
||||
[FieldType.RADIO]: { icon: Disc, label: 'Radio' },
|
||||
[FieldType.CHECKBOX]: { icon: CheckSquare, label: 'Checkbox' },
|
||||
[FieldType.DROPDOWN]: { icon: ChevronDown, label: 'Select' },
|
||||
};
|
||||
|
||||
export const FieldIcon = ({ fieldMeta, type }: FieldIconProps) => {
|
||||
if (type === 'SIGNATURE' || type === 'FREE_SIGNATURE') {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'text-field-card-foreground font-signature flex items-center justify-center gap-x-1 text-[clamp(0.575rem,25cqw,1.2rem)]',
|
||||
)}
|
||||
>
|
||||
<Trans>Signature</Trans>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const Icon = fieldIcons[type]?.icon;
|
||||
let label;
|
||||
|
||||
if (fieldMeta && (type === 'TEXT' || type === 'NUMBER')) {
|
||||
if (type === 'TEXT' && 'text' in fieldMeta && fieldMeta.text && !fieldMeta.label) {
|
||||
label =
|
||||
fieldMeta.text.length > 20 ? fieldMeta.text.substring(0, 20) + '...' : fieldMeta.text;
|
||||
} else if (fieldMeta.label) {
|
||||
label =
|
||||
fieldMeta.label.length > 20 ? fieldMeta.label.substring(0, 20) + '...' : fieldMeta.label;
|
||||
} else {
|
||||
label = fieldIcons[type]?.label;
|
||||
}
|
||||
} else {
|
||||
label = fieldIcons[type]?.label;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-field-card-foreground flex items-center justify-center gap-x-1.5 text-[clamp(0.425rem,25cqw,0.825rem)]">
|
||||
<Icon className="h-[clamp(0.625rem,20cqw,0.925rem)] w-[clamp(0.625rem,20cqw,0.925rem)]" />{' '}
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -41,7 +41,6 @@ import { RadioFieldAdvancedSettings } from './field-items-advanced-settings/radi
|
||||
import { TextFieldAdvancedSettings } from './field-items-advanced-settings/text-field';
|
||||
|
||||
export type FieldAdvancedSettingsProps = {
|
||||
teamId?: number;
|
||||
title: MessageDescriptor;
|
||||
description: MessageDescriptor;
|
||||
field: FieldFormType;
|
||||
@ -130,6 +129,7 @@ const getDefaultState = (fieldType: FieldType): FieldMeta => {
|
||||
validationLength: 0,
|
||||
required: false,
|
||||
readOnly: false,
|
||||
direction: 'vertical',
|
||||
};
|
||||
case FieldType.DROPDOWN:
|
||||
return {
|
||||
@ -229,11 +229,19 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
|
||||
return (
|
||||
<div ref={ref} className="flex h-full flex-col">
|
||||
<DocumentFlowFormContainerHeader title={title} description={description} />
|
||||
|
||||
<DocumentFlowFormContainerContent>
|
||||
{isDocumentPdfLoaded &&
|
||||
fields.map((field, index) => (
|
||||
fields.map((localField, index) => (
|
||||
<span key={index} className="opacity-75 active:pointer-events-none">
|
||||
<FieldItem key={index} field={field} disabled={true} />
|
||||
<FieldItem
|
||||
key={index}
|
||||
field={localField}
|
||||
disabled={true}
|
||||
fieldClassName={
|
||||
localField.formId === field.formId ? 'ring-red-400' : 'ring-neutral-200'
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
|
||||
@ -303,6 +311,7 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
|
||||
/>
|
||||
))
|
||||
.otherwise(() => null)}
|
||||
|
||||
{errors.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<ul>
|
||||
@ -315,6 +324,7 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
|
||||
</div>
|
||||
)}
|
||||
</DocumentFlowFormContainerContent>
|
||||
|
||||
<DocumentFlowFormContainerFooter className="mt-auto">
|
||||
<DocumentFlowFormContainerActions
|
||||
goNextLabel={msg`Save`}
|
||||
|
||||
@ -1,26 +1,28 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import { CopyPlus, Settings2, Trash } from 'lucide-react';
|
||||
import { CopyPlus, Settings2, SquareStack, Trash } from 'lucide-react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Rnd } from 'react-rnd';
|
||||
import { match } from 'ts-pattern';
|
||||
import { useSearchParams } from 'react-router';
|
||||
|
||||
import { useElementBounds } from '@documenso/lib/client-only/hooks/use-element-bounds';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
|
||||
import { useSignerColors } from '../../lib/signer-colors';
|
||||
import { useRecipientColors } from '../../lib/recipient-colors';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { CheckboxField } from './advanced-fields/checkbox';
|
||||
import { RadioField } from './advanced-fields/radio';
|
||||
import { FieldIcon } from './field-icon';
|
||||
import { FieldContent } from './field-content';
|
||||
import type { TDocumentFlowFormSchema } from './types';
|
||||
|
||||
type Field = TDocumentFlowFormSchema['fields'][0];
|
||||
|
||||
export type FieldItemProps = {
|
||||
field: Field;
|
||||
fieldClassName?: string;
|
||||
passive?: boolean;
|
||||
disabled?: boolean;
|
||||
minHeight?: number;
|
||||
@ -31,18 +33,24 @@ export type FieldItemProps = {
|
||||
onMove?: (_node: HTMLElement) => void;
|
||||
onRemove?: () => void;
|
||||
onDuplicate?: () => void;
|
||||
onDuplicateAllPages?: () => void;
|
||||
onAdvancedSettings?: () => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
onMouseEnter?: () => void;
|
||||
onMouseLeave?: () => void;
|
||||
recipientIndex?: number;
|
||||
hideRecipients?: boolean;
|
||||
hasErrors?: boolean;
|
||||
active?: boolean;
|
||||
onFieldActivate?: () => void;
|
||||
onFieldDeactivate?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* The item when editing fields??
|
||||
*/
|
||||
export const FieldItem = ({
|
||||
fieldClassName,
|
||||
field,
|
||||
passive,
|
||||
disabled,
|
||||
@ -54,16 +62,19 @@ export const FieldItem = ({
|
||||
onMove,
|
||||
onRemove,
|
||||
onDuplicate,
|
||||
onDuplicateAllPages,
|
||||
onAdvancedSettings,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onAdvancedSettings,
|
||||
recipientIndex = 0,
|
||||
hideRecipients = false,
|
||||
hasErrors,
|
||||
active,
|
||||
onFieldActivate,
|
||||
onFieldDeactivate,
|
||||
}: FieldItemProps) => {
|
||||
const { _ } = useLingui();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const [coords, setCoords] = useState({
|
||||
pageX: 0,
|
||||
pageY: 0,
|
||||
@ -71,9 +82,15 @@ export const FieldItem = ({
|
||||
pageWidth: defaultWidth || 0,
|
||||
});
|
||||
const [settingsActive, setSettingsActive] = useState(false);
|
||||
const $el = useRef(null);
|
||||
const $el = useRef<HTMLDivElement>(null);
|
||||
|
||||
const signerStyles = useSignerColors(recipientIndex);
|
||||
const $pageBounds = useElementBounds(
|
||||
`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`,
|
||||
);
|
||||
|
||||
const signerStyles = useRecipientColors(recipientIndex);
|
||||
|
||||
const isDevMode = searchParams.get('devmode') === 'true';
|
||||
|
||||
const advancedField = [
|
||||
'NUMBER',
|
||||
@ -209,7 +226,7 @@ export const FieldItem = ({
|
||||
return createPortal(
|
||||
<Rnd
|
||||
key={coords.pageX + coords.pageY + coords.pageHeight + coords.pageWidth}
|
||||
className={cn('group', {
|
||||
className={cn('dark-mode-disabled group', {
|
||||
'pointer-events-none': passive,
|
||||
'pointer-events-none cursor-not-allowed opacity-75': disabled,
|
||||
'z-50': active && !disabled,
|
||||
@ -221,12 +238,15 @@ export const FieldItem = ({
|
||||
default={{
|
||||
x: coords.pageX,
|
||||
y: coords.pageY,
|
||||
height: fixedSize ? '' : coords.pageHeight,
|
||||
width: fixedSize ? '' : coords.pageWidth,
|
||||
height: fixedSize ? 'auto' : coords.pageHeight,
|
||||
width: fixedSize ? 'auto' : coords.pageWidth,
|
||||
}}
|
||||
maxWidth={fixedSize && $pageBounds?.width ? $pageBounds.width - coords.pageX : undefined}
|
||||
bounds={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`}
|
||||
onDragStart={() => onFieldActivate?.()}
|
||||
onResizeStart={() => onFieldActivate?.()}
|
||||
onMouseEnter={() => onFocus?.()}
|
||||
onMouseLeave={() => onBlur?.()}
|
||||
enableResizing={!fixedSize}
|
||||
resizeHandleStyles={{
|
||||
bottom: { bottom: -8, cursor: 'ns-resize' },
|
||||
@ -260,11 +280,12 @@ export const FieldItem = ({
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-full w-full items-center justify-center bg-white',
|
||||
!hasErrors && signerStyles.default.base,
|
||||
!hasErrors && signerStyles.default.fieldItem,
|
||||
'group/field-item relative flex h-full w-full items-center justify-center rounded-[2px] bg-white/90 px-2 ring-2 transition-colors',
|
||||
!hasErrors && signerStyles.base,
|
||||
!hasErrors && signerStyles.fieldItem,
|
||||
fieldClassName,
|
||||
{
|
||||
'rounded-lg border border-red-400 bg-red-400/20 shadow-[0_0_0_5px_theme(colors.red.500/10%),0_0_0_2px_theme(colors.red.500/40%),0_0_0_0.5px_theme(colors.red.500)]':
|
||||
'rounded-[2px] border bg-red-400/20 shadow-[0_0_0_5px_theme(colors.red.500/10%),0_0_0_2px_theme(colors.red.500/40%),0_0_0_0.5px_theme(colors.red.500)] ring-red-400':
|
||||
hasErrors,
|
||||
},
|
||||
!fixedSize && '[container-type:size]',
|
||||
@ -279,37 +300,38 @@ export const FieldItem = ({
|
||||
ref={$el}
|
||||
data-field-id={field.nativeId}
|
||||
>
|
||||
{match(field.type)
|
||||
.with('CHECKBOX', () => <CheckboxField field={field} />)
|
||||
.with('RADIO', () => <RadioField field={field} />)
|
||||
.otherwise(() => (
|
||||
<FieldIcon fieldMeta={field.fieldMeta} type={field.type} />
|
||||
))}
|
||||
<FieldContent field={field} />
|
||||
|
||||
{!hideRecipients && (
|
||||
<div className="absolute -right-5 top-0 z-20 hidden h-full w-5 items-center justify-center group-hover:flex">
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-5 w-5 flex-col items-center justify-center rounded-r-md text-[0.5rem] font-bold text-white',
|
||||
signerStyles.default.fieldItemInitials,
|
||||
{
|
||||
'!opacity-50': disabled || passive,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{(field.signerEmail?.charAt(0)?.toUpperCase() ?? '') +
|
||||
(field.signerEmail?.charAt(1)?.toUpperCase() ?? '')}
|
||||
</div>
|
||||
{/* On hover, display recipient initials on side of field. */}
|
||||
<div className="absolute -right-5 top-0 z-20 hidden h-full w-5 items-center justify-center group-hover:flex">
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-5 w-5 flex-col items-center justify-center rounded-r-md text-[0.5rem] font-bold text-white opacity-0 transition duration-200 group-hover/field-item:opacity-100',
|
||||
signerStyles.fieldItemInitials,
|
||||
{
|
||||
'!opacity-50': disabled || passive,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{(field.signerEmail?.charAt(0)?.toUpperCase() ?? '') +
|
||||
(field.signerEmail?.charAt(1)?.toUpperCase() ?? '')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isDevMode && (
|
||||
<div className="text-muted-foreground absolute -top-6 left-0 right-0 text-center text-[10px]">
|
||||
{`x: ${field.pageX.toFixed(2)}, y: ${field.pageY.toFixed(2)}`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!disabled && settingsActive && (
|
||||
<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">
|
||||
<div className="absolute z-[60] mt-1 flex w-full items-center justify-center">
|
||||
<div className="group flex items-center justify-evenly gap-x-1 rounded-md border bg-gray-900 p-0.5">
|
||||
{advancedField && (
|
||||
<button
|
||||
className="dark:text-muted-foreground/50 dark:hover:text-muted-foreground dark:hover:bg-foreground/10 rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||
title={_(msg`Advanced settings`)}
|
||||
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||
onClick={onAdvancedSettings}
|
||||
onTouchEnd={onAdvancedSettings}
|
||||
>
|
||||
@ -318,7 +340,8 @@ export const FieldItem = ({
|
||||
)}
|
||||
|
||||
<button
|
||||
className="dark:text-muted-foreground/50 dark:hover:text-muted-foreground dark:hover:bg-foreground/10 rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||
title={_(msg`Duplicate`)}
|
||||
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||
onClick={onDuplicate}
|
||||
onTouchEnd={onDuplicate}
|
||||
>
|
||||
@ -326,7 +349,17 @@ export const FieldItem = ({
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="dark:text-muted-foreground/50 dark:hover:text-muted-foreground dark:hover:bg-foreground/10 rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||
title={_(msg`Duplicate on all pages`)}
|
||||
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||
onClick={onDuplicateAllPages}
|
||||
onTouchEnd={onDuplicateAllPages}
|
||||
>
|
||||
<SquareStack className="h-3 w-3" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
title={_(msg`Remove`)}
|
||||
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||
onClick={onRemove}
|
||||
onTouchEnd={onRemove}
|
||||
>
|
||||
|
||||
@ -44,6 +44,9 @@ export const CheckboxFieldAdvancedSettings = ({
|
||||
const [required, setRequired] = useState(fieldState.required ?? false);
|
||||
const [validationLength, setValidationLength] = useState(fieldState.validationLength ?? 0);
|
||||
const [validationRule, setValidationRule] = useState(fieldState.validationRule ?? '');
|
||||
const [direction, setDirection] = useState<'vertical' | 'horizontal'>(
|
||||
fieldState.direction ?? 'vertical',
|
||||
);
|
||||
|
||||
const handleToggleChange = (field: keyof CheckboxFieldMeta, value: string | boolean) => {
|
||||
const readOnly = field === 'readOnly' ? Boolean(value) : Boolean(fieldState.readOnly);
|
||||
@ -52,11 +55,14 @@ export const CheckboxFieldAdvancedSettings = ({
|
||||
field === 'validationRule' ? String(value) : String(fieldState.validationRule);
|
||||
const validationLength =
|
||||
field === 'validationLength' ? Number(value) : Number(fieldState.validationLength);
|
||||
const currentDirection =
|
||||
field === 'direction' && String(value) === 'horizontal' ? 'horizontal' : 'vertical';
|
||||
|
||||
setReadOnly(readOnly);
|
||||
setRequired(required);
|
||||
setValidationRule(validationRule);
|
||||
setValidationLength(validationLength);
|
||||
setDirection(currentDirection);
|
||||
|
||||
const errors = validateCheckboxField(
|
||||
values.map((item) => item.value),
|
||||
@ -65,6 +71,7 @@ export const CheckboxFieldAdvancedSettings = ({
|
||||
required,
|
||||
validationRule,
|
||||
validationLength,
|
||||
direction: currentDirection,
|
||||
type: 'checkbox',
|
||||
},
|
||||
);
|
||||
@ -86,6 +93,7 @@ export const CheckboxFieldAdvancedSettings = ({
|
||||
required,
|
||||
validationRule,
|
||||
validationLength,
|
||||
direction: direction,
|
||||
type: 'checkbox',
|
||||
},
|
||||
);
|
||||
@ -137,6 +145,29 @@ export const CheckboxFieldAdvancedSettings = ({
|
||||
onChange={(e) => handleFieldChange('label', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-2">
|
||||
<Label>
|
||||
<Trans>Direction</Trans>
|
||||
</Label>
|
||||
<Select
|
||||
value={fieldState.direction ?? 'vertical'}
|
||||
onValueChange={(val) => handleToggleChange('direction', val)}
|
||||
>
|
||||
<SelectTrigger className="text-muted-foreground bg-background mt-2 w-full">
|
||||
<SelectValue placeholder={_(msg`Select direction`)} />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper">
|
||||
<SelectItem value="vertical">
|
||||
<Trans>Vertical</Trans>
|
||||
</SelectItem>
|
||||
<SelectItem value="horizontal">
|
||||
<Trans>Horizontal</Trans>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center gap-x-4">
|
||||
<div className="flex w-2/3 flex-col">
|
||||
<Label>
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { FieldType, type Prisma } from '@prisma/client';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
|
||||
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Card, CardContent } from '../card';
|
||||
import { FRIENDLY_FIELD_TYPE } from './types';
|
||||
|
||||
export type ShowFieldItemProps = {
|
||||
field: Prisma.FieldGetPayload<null>;
|
||||
recipients: Prisma.RecipientGetPayload<null>[];
|
||||
};
|
||||
|
||||
export const ShowFieldItem = ({ field }: ShowFieldItemProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const coords = useFieldPageCoords(field);
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={cn('pointer-events-none absolute z-10 opacity-75')}
|
||||
style={{
|
||||
top: `${coords.y}px`,
|
||||
left: `${coords.x}px`,
|
||||
height: `${coords.height}px`,
|
||||
width: `${coords.width}px`,
|
||||
}}
|
||||
>
|
||||
<Card className={cn('bg-background h-full w-full [container-type:size]')}>
|
||||
<CardContent
|
||||
className={cn(
|
||||
'text-muted-foreground/50 flex h-full w-full flex-col items-center justify-center p-0 text-[clamp(0.575rem,1.8cqw,1.2rem)] leading-none',
|
||||
field.type === FieldType.SIGNATURE && 'font-signature',
|
||||
)}
|
||||
>
|
||||
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])}
|
||||
|
||||
{/* <p className="text-muted-foreground/50 w-full truncate text-center text-xs">
|
||||
{signerEmail}
|
||||
</p> */}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user