mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
160 lines
5.9 KiB
TypeScript
160 lines
5.9 KiB
TypeScript
import { validateCheckboxLength } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
|
|
import type { TCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
|
|
import { cn } from '@documenso/ui/lib/utils';
|
|
import { Button } from '@documenso/ui/primitives/button';
|
|
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@documenso/ui/primitives/dialog';
|
|
import { Form, FormControl, FormField, FormItem } from '@documenso/ui/primitives/form/form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { msg } from '@lingui/core/macro';
|
|
import { Plural, Trans } from '@lingui/react/macro';
|
|
import { createCallable } from 'react-call';
|
|
import { useForm, useWatch } from 'react-hook-form';
|
|
import { match } from 'ts-pattern';
|
|
import { z } from 'zod';
|
|
|
|
export type SignFieldCheckboxDialogProps = {
|
|
fieldMeta: TCheckboxFieldMeta;
|
|
validationRule: '>=' | '=' | '<=';
|
|
validationLength: number;
|
|
preselectedIndices: number[];
|
|
};
|
|
|
|
export const SignFieldCheckboxDialog = createCallable<SignFieldCheckboxDialogProps, number[] | null>(
|
|
({ call, fieldMeta, validationRule, validationLength, preselectedIndices }) => {
|
|
const ZSignFieldCheckboxFormSchema = z
|
|
.object({
|
|
values: z.array(
|
|
z.object({
|
|
checked: z.boolean(),
|
|
value: z.string(),
|
|
}),
|
|
),
|
|
})
|
|
.superRefine((data, ctx) => {
|
|
// Allow unselecting all options if the field is not required even if
|
|
// validation is not met.
|
|
if (!fieldMeta.required && data.values.every((value) => !value.checked)) {
|
|
return;
|
|
}
|
|
|
|
const numberOfSelectedValues = data.values.filter((value) => value.checked).length;
|
|
|
|
const isValid = validateCheckboxLength(numberOfSelectedValues, validationRule, validationLength);
|
|
|
|
if (!isValid) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: msg`Validation failed`.id,
|
|
});
|
|
}
|
|
});
|
|
|
|
const form = useForm<z.infer<typeof ZSignFieldCheckboxFormSchema>>({
|
|
resolver: zodResolver(ZSignFieldCheckboxFormSchema),
|
|
defaultValues: {
|
|
values: (fieldMeta.values || []).map((value, index) => ({
|
|
checked: preselectedIndices.includes(index) || false,
|
|
value: value.value,
|
|
})),
|
|
},
|
|
});
|
|
|
|
const formValues = useWatch({
|
|
control: form.control,
|
|
});
|
|
|
|
return (
|
|
<Dialog open={true} onOpenChange={(value) => (!value ? call.end(null) : null)}>
|
|
<DialogContent position="center">
|
|
<DialogHeader>
|
|
<DialogTitle>{fieldMeta.label || <Trans>Select Options</Trans>}</DialogTitle>
|
|
|
|
<DialogDescription
|
|
className={cn('mt-4', {
|
|
'text-destructive': Object.keys(form.formState.errors).length > 0,
|
|
})}
|
|
>
|
|
{match(validationRule)
|
|
.with('>=', () => (
|
|
<Plural value={validationLength} one="Select at least # option" other="Select at least # options" />
|
|
))
|
|
.with('=', () => (
|
|
<Plural value={validationLength} one="Select exactly # option" other="Select exactly # options" />
|
|
))
|
|
.with('<=', () => (
|
|
<Plural value={validationLength} one="Select at most # option" other="Select at most # options" />
|
|
))
|
|
.exhaustive()}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<Form {...form}>
|
|
<form
|
|
onSubmit={form.handleSubmit((data) =>
|
|
call.end(data.values.map((value, i) => (value.checked ? i : null)).filter((value) => value !== null)),
|
|
)}
|
|
>
|
|
<fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
|
|
<ul className="space-y-3">
|
|
{(formValues.values || []).map((value, index) => (
|
|
<li key={`checkbox-${index}`}>
|
|
<FormField
|
|
control={form.control}
|
|
name={`values.${index}`}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<div className="flex items-center">
|
|
<Checkbox
|
|
id={`checkbox-value-${index}`}
|
|
className="h-5 w-5 border-foreground/30 data-[state=checked]:bg-primary"
|
|
checked={field.value.checked}
|
|
onCheckedChange={(checked) => {
|
|
field.onChange({
|
|
...field.value,
|
|
checked,
|
|
});
|
|
}}
|
|
/>
|
|
|
|
<label
|
|
className="ml-2 w-full text-muted-foreground text-sm"
|
|
htmlFor={`checkbox-value-${index}`}
|
|
>
|
|
{value.value}
|
|
</label>
|
|
</div>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
<DialogFooter>
|
|
<Button type="button" variant="secondary" onClick={() => call.end(null)}>
|
|
<Trans>Cancel</Trans>
|
|
</Button>
|
|
|
|
<Button type="submit">
|
|
<Trans>Confirm</Trans>
|
|
</Button>
|
|
</DialogFooter>
|
|
</fieldset>
|
|
</form>
|
|
</Form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
},
|
|
);
|