Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { FieldTypes } from "@calcom/app-store/routing-forms/lib/FieldTypes";
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
import { LearnMoreLink } from "@calcom/features/eventtypes/components/LearnMoreLink";
import { getFieldIdentifier } from "@calcom/features/form-builder/utils/getFieldIdentifier";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import classNames from "@calcom/ui/classNames";
import { Button } from "@calcom/ui/components/button";
Expand Down Expand Up @@ -96,39 +95,6 @@ function Field({
}
deleteField={router ? null : deleteField}>
<FormCardBody>
<div className="mb-3 w-full">
<TextField
data-testid={`${hookFieldNamespace}.label`}
disabled={!!router}
label="Label"
className="grow"
placeholder={t("this_is_what_your_users_would_see")}
defaultValue={label || routerField?.label || "Field"}
required
{...hookForm.register(`${hookFieldNamespace}.label`)}
onChange={(e) => {
const newLabel = e.target.value;
// Use label from useWatch which is guaranteed to be the previous value
// since useWatch updates reactively (after re-render), not synchronously
const previousLabel = label || "";
hookForm.setValue(`${hookFieldNamespace}.label`, newLabel, {
shouldDirty: true,
});
const currentIdentifier = hookForm.getValues(`${hookFieldNamespace}.identifier`);
// Only auto-update identifier if it was auto-generated from the previous label
// This preserves manual identifier changes
const isIdentifierGeneratedFromPreviousLabel =
currentIdentifier === getFieldIdentifier(previousLabel).toLowerCase();
if (!currentIdentifier || isIdentifierGeneratedFromPreviousLabel) {
hookForm.setValue(
`${hookFieldNamespace}.identifier`,
getFieldIdentifier(newLabel).toLowerCase(),
{ shouldDirty: true }
);
}
}}
/>
</div>
<div className="mb-3 w-full">
<TextField
disabled={!!router}
Expand All @@ -143,14 +109,26 @@ function Field({
name={`${hookFieldNamespace}.identifier`}
required
placeholder={t("identifies_name_field")}
value={identifier || routerField?.identifier || label || routerField?.label || ""}
value={identifier || routerField?.identifier || ""}
onChange={(e) => {
hookForm.setValue(`${hookFieldNamespace}.identifier`, e.target.value.toLowerCase(), {
shouldDirty: true,
});
}}
/>
</div>
<div className="mb-3 w-full">
<TextField
data-testid={`${hookFieldNamespace}.label`}
disabled={!!router}
label="Label"
className="grow"
placeholder={t("this_is_what_your_users_would_see")}
defaultValue={label || routerField?.label || "Field"}
required
{...hookForm.register(`${hookFieldNamespace}.label`)}
/>
</div>
<div className="mb-3 w-full">
<Controller
name={`${hookFieldNamespace}.type`}
Expand Down Expand Up @@ -208,7 +186,7 @@ function Field({
}}
/>
</div>
{["select", "multiselect"].includes(fieldType) ? (
{["select", "multiselect", "radio", "checkbox"].includes(fieldType) ? (
<div className="bg-cal-muted w-full rounded-[10px] p-2">
<Label className="text-subtle">{t("options")}</Label>
<MultiOptionInput
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ function getWidgetsWithoutFactory(_configFor: ConfigFor) {
email: {
...BasicConfig.widgets.text,
},
url: {
...BasicConfig.widgets.text,
},
address: {
...BasicConfig.widgets.text,
},
multiemail: {
...BasicConfig.widgets.text,
},
radio: {
...BasicConfig.widgets.select,
},
checkbox: {
...BasicConfig.widgets.multiselect,
},
};
return widgetsWithoutFactory;
}
Expand Down Expand Up @@ -42,6 +57,36 @@ function getTypes(configFor: ConfigFor) {
...BasicConfig.types.text.widgets,
},
},
url: {
...BasicConfig.types.text,
widgets: {
...BasicConfig.types.text.widgets,
},
},
address: {
...BasicConfig.types.text,
widgets: {
...BasicConfig.types.text.widgets,
},
},
multiemail: {
...BasicConfig.types.text,
widgets: {
...BasicConfig.types.text.widgets,
},
},
radio: {
...BasicConfig.types.select,
widgets: {
...BasicConfig.types.select.widgets,
},
},
checkbox: {
...BasicConfig.types.multiselect,
widgets: {
...BasicConfig.types.multiselect.widgets,
},
},
multiselect: {
...BasicConfig.types.multiselect,
widgets: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,31 @@ function withFactoryWidgets(widgets: WidgetsWithoutFactory) {
...widgets.text,
factory: EmailFactory,
},
url: {
...widgets.text,
factory: (props) => {
if (!props) {
return <div />;
}
return <TextWidget type="url" {...props} />;
},
},
address: {
...widgets.text,
factory: TextFactory,
},
multiemail: {
...widgets.text,
factory: TextFactory,
},
radio: {
...widgets.select,
factory: SelectFactory,
} as SelectWidgetType,
checkbox: {
...widgets.multiselect,
factory: MultiSelectFactory,
} as SelectWidgetType,
};
return widgetsWithFactory;
}
Expand Down
30 changes: 30 additions & 0 deletions packages/app-store/routing-forms/lib/FieldTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ export const enum RoutingFormFieldType {
TEXTAREA = "textarea",
SINGLE_SELECT = "select",
MULTI_SELECT = "multiselect",
RADIO = "radio",
CHECKBOX = "checkbox",
PHONE = "phone",
EMAIL = "email",
URL = "url",
ADDRESS = "address",
MULTI_EMAIL = "multiemail",
Comment on lines +7 to +13
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

radio/checkbox are option-based field types, but webhook response formatting currently only treats select/multiselect as options fields (see isOptionsField in routing-forms/lib/formSubmissionUtils.ts). As a result, submissions for these new types will likely emit raw ids/values instead of the {label,id} mapping used for other option fields. Update the options-field detection/formatting to include radio and checkbox so webhook payloads remain consistent across option-based inputs.

Copilot uses AI. Check for mistakes.
}

export const isValidRoutingFormFieldType = (type: string): type is RoutingFormFieldType => {
Expand All @@ -15,8 +20,13 @@ export const isValidRoutingFormFieldType = (type: string): type is RoutingFormFi
RoutingFormFieldType.TEXTAREA,
RoutingFormFieldType.SINGLE_SELECT,
RoutingFormFieldType.MULTI_SELECT,
RoutingFormFieldType.RADIO,
RoutingFormFieldType.CHECKBOX,
RoutingFormFieldType.PHONE,
RoutingFormFieldType.EMAIL,
RoutingFormFieldType.URL,
RoutingFormFieldType.ADDRESS,
RoutingFormFieldType.MULTI_EMAIL,
].includes(type as RoutingFormFieldType);
};

Expand All @@ -41,6 +51,14 @@ export const FieldTypes = [
label: "Multiple choice selection",
value: RoutingFormFieldType.MULTI_SELECT,
},
{
label: "Radio",
value: RoutingFormFieldType.RADIO,
},
{
label: "Checkbox",
value: RoutingFormFieldType.CHECKBOX,
},
{
label: "Phone",
value: RoutingFormFieldType.PHONE,
Expand All @@ -49,4 +67,16 @@ export const FieldTypes = [
label: "Email",
value: RoutingFormFieldType.EMAIL,
},
{
label: "URL",
value: RoutingFormFieldType.URL,
},
{
label: "Address",
value: RoutingFormFieldType.ADDRESS,
},
{
label: "Multiple emails",
value: RoutingFormFieldType.MULTI_EMAIL,
},
] as const;
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@ export function getQueryBuilderConfigForFormFields(form: Pick<RoutingForm, "fiel
valueSources: ["value"],
fieldSettings: {
// IMPORTANT: listValues must be undefined for non-select/multiselect fields otherwise RAQB doesn't like it. It ends up considering all the text values as per the listValues too which could be empty as well making all values invalid
listValues: fieldType === "select" || fieldType === "multiselect" ? options : undefined,
listValues:
fieldType === "select" ||
fieldType === "multiselect" ||
fieldType === "radio" ||
fieldType === "checkbox"
? options
: undefined,
},
};
} else {
Expand Down
4 changes: 2 additions & 2 deletions packages/app-store/routing-forms/lib/transformResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function getFieldResponseForJsonLogic({
}
return value;
}
if (field.type === "multiselect") {
if (field.type === "multiselect" || field.type === "checkbox") {
// Could be option id(i.e. a UUIDv4) or option label for ease of prefilling
let valueOrLabelArray = value instanceof Array ? value : value.toString().split(",");

Expand All @@ -80,7 +80,7 @@ export function getFieldResponseForJsonLogic({
return valueOrLabelArray;
}

if (field.type === "select") {
if (field.type === "select" || field.type === "radio") {
const valueAsStringOrStringArray = typeof value === "number" ? String(value) : value;
const valueAsString =
valueAsStringOrStringArray instanceof Array
Expand Down
Loading