From a50ab58e7d5e7d886e83d1dc3864d2e6410a4adf Mon Sep 17 00:00:00 2001 From: Jonathan Mah Date: Tue, 20 Jan 2026 15:47:49 -0800 Subject: [PATCH 1/2] added max attendee configuration option --- .../components/tabs/advanced/FormBuilder.tsx | 19 +++++++++++++++++++ apps/web/public/static/locales/en/common.json | 2 ++ .../react-awesome-query-builder/widgets.tsx | 1 + .../bookings/lib/getBookingResponsesSchema.ts | 7 +++++++ packages/features/form-builder/Components.tsx | 17 ++++++++++++++++- .../form-builder/FormBuilderField.tsx | 1 + packages/prisma/zod-utils.ts | 6 ++++++ 7 files changed, 52 insertions(+), 1 deletion(-) diff --git a/apps/web/modules/event-types/components/tabs/advanced/FormBuilder.tsx b/apps/web/modules/event-types/components/tabs/advanced/FormBuilder.tsx index f08c51ea68e676..87925e88e2ae67 100644 --- a/apps/web/modules/event-types/components/tabs/advanced/FormBuilder.tsx +++ b/apps/web/modules/event-types/components/tabs/advanced/FormBuilder.tsx @@ -588,6 +588,7 @@ function FieldEditDialog({ //resolver: zodResolver(fieldSchema), }); const formFieldType = fieldForm.getValues("type"); + const fieldName = fieldForm.watch("name"); useEffect(() => { if (!formFieldType) { @@ -713,6 +714,24 @@ function FieldEditDialog({ ) : null} + {formFieldType === "multiemail" && fieldName === "guests" && ( + { + if (value === "") { + return undefined; + } + const parsed = Number(value); + return Number.isNaN(parsed) ? undefined : parsed; + }, + })} + containerClassName="mt-6" + label={t("max_attendees")} + type="number" + min={1} + /> + )} + {formFieldType === "email" && ( = CommonProps & { name?: string; + maxEntries?: number; }; export type TextLikeComponentPropsRAQB = diff --git a/packages/features/bookings/lib/getBookingResponsesSchema.ts b/packages/features/bookings/lib/getBookingResponsesSchema.ts index f858b84203eb04..fb50c9318543c1 100644 --- a/packages/features/bookings/lib/getBookingResponsesSchema.ts +++ b/packages/features/bookings/lib/getBookingResponsesSchema.ts @@ -268,6 +268,13 @@ function preprocess({ } const emails = emailsParsed.data; + if (typeof bookingField.maxEntries === "number" && emails.length > bookingField.maxEntries) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: m("max_attendees_allowed", { count: bookingField.maxEntries }), + }); + continue; + } emails.sort().some((item, i) => { if (item === emails[i + 1]) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("duplicate_email") }); diff --git a/packages/features/form-builder/Components.tsx b/packages/features/form-builder/Components.tsx index ed72d16e3c72bc..fd669f2dca81b5 100644 --- a/packages/features/form-builder/Components.tsx +++ b/packages/features/form-builder/Components.tsx @@ -247,7 +247,11 @@ export const Components: Record = { factory: function MultiEmail({ value, readOnly, label, setValue, ...props }) { const placeholder = props.placeholder; const { t } = useLocale(); - value = value || []; + const maxEntries = props.maxEntries; + const listValue = value || []; + const hasReachedLimit = + typeof maxEntries === "number" && maxEntries > 0 && listValue.length >= maxEntries; + value = listValue; return ( <> {value.length ? ( @@ -293,13 +297,20 @@ export const Components: Record = { color="minimal" StartIcon="user-plus" className="my-2.5" + disabled={hasReachedLimit} onClick={() => { + if (hasReachedLimit) { + return; + } value.push(""); setValue(value); }}> {t("add_another")} )} + {!readOnly && typeof maxEntries === "number" && maxEntries > 0 && ( +

{t("max_attendees_allowed", { count: maxEntries })}

+ )} ) : ( <> @@ -311,7 +322,11 @@ export const Components: Record = { color="minimal" variant="button" StartIcon="user-plus" + disabled={hasReachedLimit} onClick={() => { + if (hasReachedLimit) { + return; + } value.push(""); setValue(value); }} diff --git a/packages/features/form-builder/FormBuilderField.tsx b/packages/features/form-builder/FormBuilderField.tsx index 3288772d612f16..5c7035c15f8438 100644 --- a/packages/features/form-builder/FormBuilderField.tsx +++ b/packages/features/form-builder/FormBuilderField.tsx @@ -312,6 +312,7 @@ export const ComponentForField = ({ placeholder={field.placeholder} name={field.name} label={field.label} + maxEntries={field.maxEntries} readOnly={readOnly} value={value as string[]} setValue={setValue as (arg: typeof value) => void} diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index 9dd03b80d581d3..d17bf8477a037a 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -1091,6 +1091,12 @@ export const baseFieldSchema = z.object({ */ maxLength: z.number().optional(), + /** + * It is the maximum number of entries that can be provided for list fields. + * It is used for types like `multiemail`. + */ + maxEntries: z.number().int().min(1).optional(), + // Emails that needs to be excluded excludeEmails: excludeOrRequireEmailSchema.optional(), // Emails that need to be required From 11ff9d5ca9b64237fdaa2e95acf28a546ae32f1b Mon Sep 17 00:00:00 2001 From: Jonathan Mah Date: Tue, 20 Jan 2026 16:08:50 -0800 Subject: [PATCH 2/2] fix --- .../event-types/components/tabs/advanced/FormBuilder.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/modules/event-types/components/tabs/advanced/FormBuilder.tsx b/apps/web/modules/event-types/components/tabs/advanced/FormBuilder.tsx index 87925e88e2ae67..de6a10104180cb 100644 --- a/apps/web/modules/event-types/components/tabs/advanced/FormBuilder.tsx +++ b/apps/web/modules/event-types/components/tabs/advanced/FormBuilder.tsx @@ -722,7 +722,10 @@ function FieldEditDialog({ return undefined; } const parsed = Number(value); - return Number.isNaN(parsed) ? undefined : parsed; + if (Number.isNaN(parsed) || !Number.isInteger(parsed) || parsed < 1) { + return undefined; + } + return parsed; }, })} containerClassName="mt-6"