mirror of
https://github.com/hanzoai/sign.git
synced 2026-10-21 01:21:51 +00:00
@hookform/resolvers 5 types a zod resolver as input in, output out: the
form holds z.input of its schema, and only handleSubmit sees z.output. The
flows spelled their forms as the output and dropped the input on the floor,
where a default or a transform makes the two differ: signers' actionAuth and
distributionMethod have defaults, and field meta takes `{}` and makes it
undefined.
Each flow's schema now exports its input beside its output, and whatever
holds or passes on the form's values uses it: the clipboard and last-active
field, the field item, its content and tooltip (which re-parse the meta
anyway), the email checkboxes, and every autosave. Autosave reads
getValues(), which is what the user typed, not what the schema made of it,
so it is typed as that all the way to the mutation, whose request type is
now z.input of the request schema — what the server parses.
Three reads of form state now apply the schema's own default where the
value may be unset: `actionAuth ?? []` in both signer steps and
`distributionMethod ?? EMAIL` in the subject step. Every writer in those
forms sets the value, so nothing reachable changes. Two `value &&
onChange(value)` statements become ifs, which typescript-eslint 8 asks for.
Co-authored-by: Hanzo Dev <[email protected]>
212 lines
6.2 KiB
TypeScript
212 lines
6.2 KiB
TypeScript
import { useLingui } from '@lingui/react';
|
|
import { Trans } from '@lingui/react/macro';
|
|
import type { DocumentMeta, Signature } from '@prisma/client';
|
|
import { FieldType } from '@prisma/client';
|
|
import { ChevronDown } from 'lucide-react';
|
|
|
|
import {
|
|
DEFAULT_DOCUMENT_DATE_FORMAT,
|
|
convertToLocalSystemFormat,
|
|
} from '@hanzo/esign-lib/constants/date-formats';
|
|
import type { TFieldMetaInput } from '@hanzo/esign-lib/types/field-meta';
|
|
import { fromCheckboxValue } from '@hanzo/esign-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?: TFieldMetaInput | null;
|
|
signature?: Signature | null;
|
|
};
|
|
documentMeta?: Pick<DocumentMeta, '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">
|
|
<Trans>Checkbox option</Trans>
|
|
</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-sm text-[clamp(0.07rem,25cqw,0.825rem)]">
|
|
<p>
|
|
<Trans>Select</Trans>
|
|
</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 text-left text-[clamp(0.07rem,25cqw,0.825rem)] whitespace-pre-wrap 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>
|
|
);
|
|
};
|