feat: disable changing field type of routing forms if responses exist (#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>
This commit is contained in:
co-authored by
eunjae@cal.com
parent
3fd9ef366f
commit
aea0661303
@@ -2835,7 +2835,7 @@
|
||||
"salesforce_skip_entry_creation": "Skip creating {{entry}} record if they do not exist in Salesforce",
|
||||
"salesforce_if_account_does_not_exist": "If the contact does not exist under an account, create new lead from attendee",
|
||||
"salesforce_create_new_contact_under_account": "Create a new contact under an account based on email domain of attendee and existing contacts",
|
||||
"salesforce_change_record_owner_on_booking":"Change record owner on booking",
|
||||
"salesforce_change_record_owner_on_booking": "Change record owner on booking",
|
||||
"mass_assign_attributes": "Mass assign attributes",
|
||||
"reroute_preview_custom_message": "It results in showing custom message. Try changing the response to route to an event",
|
||||
"reroute_preview_external_redirect": "It results in redirecting to {{externalUrl}}. Try changing the response to route to an event",
|
||||
@@ -2888,6 +2888,7 @@
|
||||
"yes_reassign": "Yes, reassign",
|
||||
"filter_by_attributes": "Filter by attributes",
|
||||
"reassign_reason": "Reason for reassigning",
|
||||
"field_type_change_suggestion": "Changing the field type may cause data loss. Please create a new field instead.",
|
||||
"reassigned_by": "Reassigned by",
|
||||
"row": "row",
|
||||
"rows": "rows",
|
||||
@@ -2933,7 +2934,7 @@
|
||||
"routing_form_insights_assignment_reason": "Assignment Reason",
|
||||
"access_denied": "Access Denied",
|
||||
"salesforce_route_to_owner": "Contact owner will be the Round Robin host if available",
|
||||
"salesforce_add_attendees_as":"Add new attendees as",
|
||||
"salesforce_add_attendees_as": "Add new attendees as",
|
||||
"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)",
|
||||
"salesforce_route_to_custom_lookup_field": "Route to a user that matches a lookup field on an account",
|
||||
"salesforce_option": "Salesforce Option",
|
||||
@@ -2996,7 +2997,7 @@
|
||||
"shortfall": "Shortfall",
|
||||
"routing_form": "Routing Form",
|
||||
"go_back_and_save": "Go back and save",
|
||||
"save_changes":"Save changes",
|
||||
"save_changes": "Save changes",
|
||||
"leave_without_saving": "Leave without saving",
|
||||
"leave_without_saving_description": "Are you sure you want to leave without saving changes to your routing form?",
|
||||
"something_unexpected_occurred": "Something unexpected occurred",
|
||||
@@ -3031,9 +3032,9 @@
|
||||
"authorize": "Authorize",
|
||||
"next": "Next",
|
||||
"hide": "Hide",
|
||||
"show":"Show",
|
||||
"utm_params":"UTM parameters",
|
||||
"no_utm_params":"No UTM parameters available.",
|
||||
"show": "Show",
|
||||
"utm_params": "UTM parameters",
|
||||
"no_utm_params": "No UTM parameters available.",
|
||||
"asc": "Asc",
|
||||
"desc": "Desc",
|
||||
"verify_email_change": "Verify email change",
|
||||
@@ -3103,7 +3104,7 @@
|
||||
"user_has_no_team_yet": "You don't have a team yet",
|
||||
"no_team_members": "You don't have team members yet",
|
||||
"recurring_event_doesnt_support_seats": "Recurring event doesn't support seats feature. Make it non-recurring to enable seats.",
|
||||
"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.",
|
||||
"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.",
|
||||
"seats_doesnt_support_recurring": "Seats feature doesn't support recurring event. Disable seats feature to make it recurring.",
|
||||
"recurring_event_seats_error": "Recurring event doesn't support seats feature. Disable seats feature or make the event non-recurring.",
|
||||
"segment": "Segment",
|
||||
@@ -3181,8 +3182,8 @@
|
||||
"resend": "Resend",
|
||||
"verification_email_sent": "Verification email sent",
|
||||
"select_all_members": "Select all members",
|
||||
"select_all_members_tooltip":"Selecting all members will add all future team members when they are added to the team.",
|
||||
"form_settings":"Form Settings",
|
||||
"select_all_members_tooltip": "Selecting all members will add all future team members when they are added to the team.",
|
||||
"form_settings": "Form Settings",
|
||||
"remove_whitelist_status": "Remove whitelist status",
|
||||
"whitelist_user_workflows": "Whitelist user workflows",
|
||||
"user_workflows_whitelisted": "User workflows whitelisted",
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
MultiOptionInput,
|
||||
} from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
@@ -36,6 +37,7 @@ function Field({
|
||||
moveUp,
|
||||
moveDown,
|
||||
appUrl,
|
||||
disableTypeChange,
|
||||
}: {
|
||||
fieldIndex: number;
|
||||
hookForm: HookForm;
|
||||
@@ -53,6 +55,7 @@ function Field({
|
||||
fn: () => void;
|
||||
};
|
||||
appUrl: string;
|
||||
disableTypeChange: boolean;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -123,32 +126,53 @@ function Field({
|
||||
defaultValue={routerField?.type}
|
||||
render={({ field: { value, onChange } }) => {
|
||||
const defaultValue = FieldTypes.find((fieldType) => fieldType.value === value);
|
||||
return (
|
||||
<SelectField
|
||||
maxMenuHeight={200}
|
||||
styles={{
|
||||
singleValue: (baseStyles) => ({
|
||||
...baseStyles,
|
||||
fontSize: "14px",
|
||||
}),
|
||||
option: (baseStyles) => ({
|
||||
...baseStyles,
|
||||
fontSize: "14px",
|
||||
}),
|
||||
}}
|
||||
label="Type"
|
||||
isDisabled={!!router}
|
||||
containerClassName="data-testid-field-type"
|
||||
options={FieldTypes}
|
||||
onChange={(option) => {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
onChange(option.value);
|
||||
}}
|
||||
defaultValue={defaultValue}
|
||||
/>
|
||||
);
|
||||
if (disableTypeChange) {
|
||||
return (
|
||||
<div className="data-testid-field-type">
|
||||
<Label htmlFor="field-type-button">{t("type")}</Label>
|
||||
<Tooltip content={t("field_type_change_suggestion")}>
|
||||
<Button
|
||||
type="button"
|
||||
disabled
|
||||
color="secondary"
|
||||
className={classNames(
|
||||
"h-8 w-full justify-between text-left text-sm",
|
||||
!!router && "bg-subtle cursor-not-allowed"
|
||||
)}>
|
||||
<span className="text-default">{defaultValue?.label || "Select field type"}</span>
|
||||
<Icon name="chevron-down" className="text-default h-4 w-4" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<SelectField
|
||||
maxMenuHeight={200}
|
||||
styles={{
|
||||
singleValue: (baseStyles) => ({
|
||||
...baseStyles,
|
||||
fontSize: "14px",
|
||||
}),
|
||||
option: (baseStyles) => ({
|
||||
...baseStyles,
|
||||
fontSize: "14px",
|
||||
}),
|
||||
}}
|
||||
label="Type"
|
||||
isDisabled={!!router}
|
||||
containerClassName="data-testid-field-type"
|
||||
options={FieldTypes}
|
||||
onChange={(option) => {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
onChange(option.value);
|
||||
}}
|
||||
defaultValue={defaultValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -213,6 +237,7 @@ const FormEdit = ({
|
||||
} = useFieldArray({
|
||||
control: hookForm.control,
|
||||
name: fieldsNamespace,
|
||||
keyName: "_id",
|
||||
});
|
||||
|
||||
const [animationRef] = useAutoAnimate<HTMLDivElement>();
|
||||
@@ -236,12 +261,15 @@ const FormEdit = ({
|
||||
<div className="w-full py-4 lg:py-8">
|
||||
<div ref={animationRef} className="flex w-full flex-col rounded-md">
|
||||
{hookFormFields.map((field, key) => {
|
||||
const existingField = Boolean((form.fields || []).find((f) => f.id === field.id));
|
||||
const hasFormResponses = (form._count?.responses ?? 0) > 0;
|
||||
return (
|
||||
<Field
|
||||
appUrl={appUrl}
|
||||
fieldIndex={key}
|
||||
hookForm={hookForm}
|
||||
hookFieldNamespace={`${fieldsNamespace}.${key}`}
|
||||
disableTypeChange={existingField && hasFormResponses}
|
||||
deleteField={{
|
||||
check: () => hookFormFields.length > 1,
|
||||
fn: () => {
|
||||
|
||||
Reference in New Issue
Block a user