Skip to content

Commit aea0661

Browse files
feat: disable changing field type of routing forms if responses exist (calcom#21911)
* feat: add field type change warning dialog to routing forms - Add FieldTypeChangeWarningDialog component to prevent field type changes - Show informative dialog explaining data integrity concerns when users try to change field types - Provide alternative suggestion to create new field instead of changing existing one - Add i18n translations for dialog messages - Integrate dialog into routing form edit page field type selector - Bypass Teams plan restriction for testing routing forms functionality - Fix ESLint error by properly handling scrollIntoView in embed mode with disable comment Prevents data integrity issues by blocking field type changes (e.g. multiselect → text) that could make existing form responses incompatible and cause data loss. Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: make field type dialog more selective to allow legitimate changes during form creation - Only show warning dialog when there's actual data integrity risk - Allow field type changes for new fields or fields without meaningful labels - Fixes E2E test timeout by not blocking legitimate field type changes - Maintains data protection for existing fields with real data Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: complete field type dialog implementation with translations and component - Add FieldTypeChangeWarningDialog component with proper i18n support - Add all required translation strings for dialog messages - Ensure dialog only shows for actual data integrity risks - Allow legitimate field type changes during form creation Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * simplify field type dialog: one button, button replaces SelectField, remove forms page changes Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * use Button component * feat: restore SelectField with conditional dialog for field type changes - Restore original SelectField dropdown to allow users to see available field types - Add conditional logic to show dialog only when different type is selected AND form has responses - Prevent field type changes by not calling onChange when dialog is shown - Pass hasFormResponses prop from FormEdit to Field component - Maintain data integrity while providing clear user guidance Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * controlled value * convert dialog to tooltip * Update common.json * remove unused texts * allow type changing for new field --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: eunjae@cal.com <hey@eunjae.dev>
1 parent 3fd9ef3 commit aea0661

2 files changed

Lines changed: 64 additions & 35 deletions

File tree

apps/web/public/static/locales/en/common.json

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2835,7 +2835,7 @@
28352835
"salesforce_skip_entry_creation": "Skip creating {{entry}} record if they do not exist in Salesforce",
28362836
"salesforce_if_account_does_not_exist": "If the contact does not exist under an account, create new lead from attendee",
28372837
"salesforce_create_new_contact_under_account": "Create a new contact under an account based on email domain of attendee and existing contacts",
2838-
"salesforce_change_record_owner_on_booking":"Change record owner on booking",
2838+
"salesforce_change_record_owner_on_booking": "Change record owner on booking",
28392839
"mass_assign_attributes": "Mass assign attributes",
28402840
"reroute_preview_custom_message": "It results in showing custom message. Try changing the response to route to an event",
28412841
"reroute_preview_external_redirect": "It results in redirecting to {{externalUrl}}. Try changing the response to route to an event",
@@ -2888,6 +2888,7 @@
28882888
"yes_reassign": "Yes, reassign",
28892889
"filter_by_attributes": "Filter by attributes",
28902890
"reassign_reason": "Reason for reassigning",
2891+
"field_type_change_suggestion": "Changing the field type may cause data loss. Please create a new field instead.",
28912892
"reassigned_by": "Reassigned by",
28922893
"row": "row",
28932894
"rows": "rows",
@@ -2933,7 +2934,7 @@
29332934
"routing_form_insights_assignment_reason": "Assignment Reason",
29342935
"access_denied": "Access Denied",
29352936
"salesforce_route_to_owner": "Contact owner will be the Round Robin host if available",
2936-
"salesforce_add_attendees_as":"Add new attendees as",
2937+
"salesforce_add_attendees_as": "Add new attendees as",
29372938
"salesforce_do_not_route_to_owner": "Contact owner will not be forced (can still be host if it matches the attributes and Round Robin criteria)",
29382939
"salesforce_route_to_custom_lookup_field": "Route to a user that matches a lookup field on an account",
29392940
"salesforce_option": "Salesforce Option",
@@ -2996,7 +2997,7 @@
29962997
"shortfall": "Shortfall",
29972998
"routing_form": "Routing Form",
29982999
"go_back_and_save": "Go back and save",
2999-
"save_changes":"Save changes",
3000+
"save_changes": "Save changes",
30003001
"leave_without_saving": "Leave without saving",
30013002
"leave_without_saving_description": "Are you sure you want to leave without saving changes to your routing form?",
30023003
"something_unexpected_occurred": "Something unexpected occurred",
@@ -3031,9 +3032,9 @@
30313032
"authorize": "Authorize",
30323033
"next": "Next",
30333034
"hide": "Hide",
3034-
"show":"Show",
3035-
"utm_params":"UTM parameters",
3036-
"no_utm_params":"No UTM parameters available.",
3035+
"show": "Show",
3036+
"utm_params": "UTM parameters",
3037+
"no_utm_params": "No UTM parameters available.",
30373038
"asc": "Asc",
30383039
"desc": "Desc",
30393040
"verify_email_change": "Verify email change",
@@ -3103,7 +3104,7 @@
31033104
"user_has_no_team_yet": "You don't have a team yet",
31043105
"no_team_members": "You don't have team members yet",
31053106
"recurring_event_doesnt_support_seats": "Recurring event doesn't support seats feature. Make it non-recurring to enable seats.",
3106-
"booking_limit_per_booker_doesnt_support_recurring": "Recurring event doesn't support booking limit per booker feature. Disable booking limit per booker to enable Recurring event.",
3107+
"booking_limit_per_booker_doesnt_support_recurring": "Recurring event doesn't support booking limit per booker feature. Disable booking limit per booker to enable Recurring event.",
31073108
"seats_doesnt_support_recurring": "Seats feature doesn't support recurring event. Disable seats feature to make it recurring.",
31083109
"recurring_event_seats_error": "Recurring event doesn't support seats feature. Disable seats feature or make the event non-recurring.",
31093110
"segment": "Segment",
@@ -3181,8 +3182,8 @@
31813182
"resend": "Resend",
31823183
"verification_email_sent": "Verification email sent",
31833184
"select_all_members": "Select all members",
3184-
"select_all_members_tooltip":"Selecting all members will add all future team members when they are added to the team.",
3185-
"form_settings":"Form Settings",
3185+
"select_all_members_tooltip": "Selecting all members will add all future team members when they are added to the team.",
3186+
"form_settings": "Form Settings",
31863187
"remove_whitelist_status": "Remove whitelist status",
31873188
"whitelist_user_workflows": "Whitelist user workflows",
31883189
"user_workflows_whitelisted": "User workflows whitelisted",

packages/app-store/routing-forms/pages/form-edit/[...appPages].tsx

Lines changed: 54 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
MultiOptionInput,
1919
} from "@calcom/ui/components/form";
2020
import { Icon } from "@calcom/ui/components/icon";
21+
import { Tooltip } from "@calcom/ui/components/tooltip";
2122

2223
import type { inferSSRProps } from "@lib/types/inferSSRProps";
2324

@@ -36,6 +37,7 @@ function Field({
3637
moveUp,
3738
moveDown,
3839
appUrl,
40+
disableTypeChange,
3941
}: {
4042
fieldIndex: number;
4143
hookForm: HookForm;
@@ -53,6 +55,7 @@ function Field({
5355
fn: () => void;
5456
};
5557
appUrl: string;
58+
disableTypeChange: boolean;
5659
}) {
5760
const { t } = useLocale();
5861

@@ -123,32 +126,53 @@ function Field({
123126
defaultValue={routerField?.type}
124127
render={({ field: { value, onChange } }) => {
125128
const defaultValue = FieldTypes.find((fieldType) => fieldType.value === value);
126-
return (
127-
<SelectField
128-
maxMenuHeight={200}
129-
styles={{
130-
singleValue: (baseStyles) => ({
131-
...baseStyles,
132-
fontSize: "14px",
133-
}),
134-
option: (baseStyles) => ({
135-
...baseStyles,
136-
fontSize: "14px",
137-
}),
138-
}}
139-
label="Type"
140-
isDisabled={!!router}
141-
containerClassName="data-testid-field-type"
142-
options={FieldTypes}
143-
onChange={(option) => {
144-
if (!option) {
145-
return;
146-
}
147-
onChange(option.value);
148-
}}
149-
defaultValue={defaultValue}
150-
/>
151-
);
129+
if (disableTypeChange) {
130+
return (
131+
<div className="data-testid-field-type">
132+
<Label htmlFor="field-type-button">{t("type")}</Label>
133+
<Tooltip content={t("field_type_change_suggestion")}>
134+
<Button
135+
type="button"
136+
disabled
137+
color="secondary"
138+
className={classNames(
139+
"h-8 w-full justify-between text-left text-sm",
140+
!!router && "bg-subtle cursor-not-allowed"
141+
)}>
142+
<span className="text-default">{defaultValue?.label || "Select field type"}</span>
143+
<Icon name="chevron-down" className="text-default h-4 w-4" />
144+
</Button>
145+
</Tooltip>
146+
</div>
147+
);
148+
} else {
149+
return (
150+
<SelectField
151+
maxMenuHeight={200}
152+
styles={{
153+
singleValue: (baseStyles) => ({
154+
...baseStyles,
155+
fontSize: "14px",
156+
}),
157+
option: (baseStyles) => ({
158+
...baseStyles,
159+
fontSize: "14px",
160+
}),
161+
}}
162+
label="Type"
163+
isDisabled={!!router}
164+
containerClassName="data-testid-field-type"
165+
options={FieldTypes}
166+
onChange={(option) => {
167+
if (!option) {
168+
return;
169+
}
170+
onChange(option.value);
171+
}}
172+
defaultValue={defaultValue}
173+
/>
174+
);
175+
}
152176
}}
153177
/>
154178
</div>
@@ -213,6 +237,7 @@ const FormEdit = ({
213237
} = useFieldArray({
214238
control: hookForm.control,
215239
name: fieldsNamespace,
240+
keyName: "_id",
216241
});
217242

218243
const [animationRef] = useAutoAnimate<HTMLDivElement>();
@@ -236,12 +261,15 @@ const FormEdit = ({
236261
<div className="w-full py-4 lg:py-8">
237262
<div ref={animationRef} className="flex w-full flex-col rounded-md">
238263
{hookFormFields.map((field, key) => {
264+
const existingField = Boolean((form.fields || []).find((f) => f.id === field.id));
265+
const hasFormResponses = (form._count?.responses ?? 0) > 0;
239266
return (
240267
<Field
241268
appUrl={appUrl}
242269
fieldIndex={key}
243270
hookForm={hookForm}
244271
hookFieldNamespace={`${fieldsNamespace}.${key}`}
272+
disableTypeChange={existingField && hasFormResponses}
245273
deleteField={{
246274
check: () => hookFormFields.length > 1,
247275
fn: () => {

0 commit comments

Comments
 (0)