Files
zeekayandHanzo Dev 252e15250f fix(forms): type the app's other forms by their schema's input
The same @hookform/resolvers 5 change, in the dialogs, settings forms, the
envelope editor and embedded authoring: `useForm<T>` pinned a form to its
schema's output, the resolver no longer matches that, so each form lets the
resolver type it, and whatever takes its control or context says input and
output — Control<Input, unknown, Output>, UseFormReturn likewise.

What the input exposed:
- zod 4 types z.preprocess's input as unknown unless the function names it.
  The distribute dialog's and the settings dialog's emailReplyTo hold a
  string or nothing, and now say so, as the two flow steps do.
- Combobox and ColorPicker already render an unset value as their
  placeholder and default; their props now allow one.
- The embed's signature types and the editor's signers' actionAuth may be
  unset in the form. The multiselect reads `?? []` and the signer check
  `?? []`, the schemas' own defaults; every writer sets both, so nothing
  reachable changes.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-19 04:21:49 -07:00

109 lines
3.4 KiB
TypeScript

import { useId } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { DocumentSigningOrder, type Recipient, RecipientRole } from '@prisma/client';
import type { UseFormReturn } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { prop, sortBy } from 'remeda';
import { z } from 'zod';
import {
ZRecipientActionAuthTypesSchema,
ZRecipientAuthOptionsSchema,
} from '@hanzo/esign-lib/types/document-auth';
import type { TEditorEnvelope } from '@hanzo/esign-lib/types/envelope-editor';
import { ZRecipientEmailSchema } from '@hanzo/esign-lib/types/recipient';
const LocalRecipientSchema = z.object({
formId: z.string().min(1),
id: z.number().optional(),
email: ZRecipientEmailSchema,
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
});
type TLocalRecipient = z.infer<typeof LocalRecipientSchema>;
export const ZEditorRecipientsFormSchema = z.object({
signers: z.array(LocalRecipientSchema),
signingOrder: z.nativeEnum(DocumentSigningOrder),
allowDictateNextSigner: z.boolean().default(false),
});
export type TEditorRecipientsFormSchema = z.infer<typeof ZEditorRecipientsFormSchema>;
// What the form holds while it is edited, before the schema fills in its defaults.
export type TEditorRecipientsFormInput = z.input<typeof ZEditorRecipientsFormSchema>;
type EditorRecipientsProps = {
envelope: TEditorEnvelope;
};
type ResetFormOptions = {
recipients?: Recipient[];
documentMeta?: TEditorEnvelope['documentMeta'];
};
type UseEditorRecipientsResponse = {
form: UseFormReturn<TEditorRecipientsFormInput, unknown, TEditorRecipientsFormSchema>;
resetForm: (options?: ResetFormOptions) => void;
};
export const useEditorRecipients = ({
envelope,
}: EditorRecipientsProps): UseEditorRecipientsResponse => {
const initialId = useId();
const generateDefaultValues = (options?: ResetFormOptions) => {
const { recipients, documentMeta } = options ?? {};
const formRecipients = (recipients || envelope.recipients).map((recipient, index) => ({
id: recipient.id,
formId: String(recipient.id),
name: recipient.name,
email: recipient.email,
role: recipient.role,
signingOrder: recipient.signingOrder ?? index + 1,
actionAuth: ZRecipientAuthOptionsSchema.parse(recipient.authOptions)?.actionAuth ?? undefined,
}));
const signers: TLocalRecipient[] =
formRecipients.length > 0
? sortBy(formRecipients, [prop('signingOrder'), 'asc'], [prop('id'), 'asc'])
: [
{
formId: initialId,
name: '',
email: '',
role: RecipientRole.SIGNER,
signingOrder: 1,
actionAuth: [],
},
];
return {
signers,
signingOrder: documentMeta?.signingOrder ?? envelope.documentMeta.signingOrder,
allowDictateNextSigner:
documentMeta?.allowDictateNextSigner ?? envelope.documentMeta.allowDictateNextSigner,
};
};
const form = useForm({
defaultValues: generateDefaultValues(),
resolver: zodResolver(ZEditorRecipientsFormSchema),
mode: 'onChange', // Used for autosave purposes, maybe can try onBlur instead?
});
const resetForm = (options?: ResetFormOptions) => {
form.reset(generateDefaultValues(options));
};
return {
form,
resetForm,
};
};