Files
calendar/apps/web/modules/form-builder/components/Components.tsx
T
Eunjae LeeGitHubClaude Opus 4.5Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
bf348ee209 refactor: migrate Icon to component icons from @coss/ui/icons (#27458)
* feat: add @coss/ui/icons package and migrate static icon usages

Create a new icon system in @coss/ui/icons that wraps lucide-react icons
with controlled props (size and className only). Migrate 44 component
files from <Icon name="xxx" /> to <XxxIcon /> pattern.

This is Phase 1 of the icon migration, covering only static icon names.
Dynamic icon usages (e.g., name={variable}) are skipped for now.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate more static icons to @coss/ui/icons (batch 2)

Migrated 18 files from <Icon name="xxx" /> to <XxxIcon /> pattern:
- Event meta, insights, shell, user table bulk actions
- Webhooks, blocklist, form-builder, form components

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate more static icons to @coss/ui/icons (batch 3)

Migrated 7 files:
- CancelBooking, AddGuestsDialog, ChargeCardDialog
- ReassignDialog, RescheduleDialog
- Categories, Slider

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate more static icons to @coss/ui/icons (batch 4)

Migrated 6 files:
- Organizations: AboutOrganizationForm, PaymentStatusView, profile
- Workflows: CallDetailsSheet, SkeletonLoaderList, TimeTimeUnitInput

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate more static icons to @coss/ui/icons (batch 5)

Migrated 4 files:
- TwoFactorModalHeader
- App setups: alby, make, btcpayserver

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 6)

- confirmDialog.tsx: clock icon
- ics-feedcalendar/Setup.tsx: trash, plus icons
- hitpay/Setup.tsx: info icon
- paypal/Setup.tsx: circle-alert icons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 7)

- webhook-edit-view.tsx: external-link icon
- webhook-new-view.tsx: external-link icon
- UserDropdown.tsx: chevron-down, user, settings, moon, map, circle-help, download, blocks, log-out icons
- TopNav.tsx: settings icon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 8)

- EmailInviteForm.tsx: x icon
- InviteOptions.tsx: mail, upload, link icons
- SkeletonLoader.tsx: search, clock, user icons
- Embed.tsx: sun, arrow-left icons
- CalendarSwitch.tsx: arrow-left, rotate-cw icons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 9)

- apps-view.tsx: search icon
- categories-view.tsx: arrow-left, arrow-right icons
- DatePicker.tsx: calendar icon
- EditLocationDialog.tsx: map-pin icon
- AppPage.tsx: circle-alert, book-open, external-link, mail, file, shield, flag icons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 10)

- LicenseSelection.tsx: loader, check icons
- AppConnectionItem.tsx: circle-alert, arrow-right icons
- oauth-client-form/index.tsx: info icons (x2)
- auth/error/page.tsx: x icon
- troubleshoot/layout.tsx: loader icon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 11)

- UpgradeTipWrapper.tsx: users, refresh-ccw, user-plus icons
- PbacOptInModal.tsx: shield-check, lock, users, shield icons
- AdvancedPermissionGroup.tsx: chevron-right, info icons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 12)

- AdminAppsList.tsx: pencil icon
- AvailableTimes.tsx: calendar-x-2 icon
- BookingDetailsSheet.tsx: external-link (x2), repeat icons
- BookingCalendarContainer.tsx: chevron-left, chevron-right icons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 13)

- videos-single-view.tsx: chevron-right icon
- TroubleshooterSidebar.tsx: arrow-left icon
- RuleBuilder.tsx: filter icon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 14)

- FieldMappingBuilder.tsx: arrow-right, link icons
- IntegrationAttributeSyncCard.tsx: key icon
- onboarding-view.tsx: loader icon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 15)

- team-profile-view.tsx: copy icon
- DateRangeFilter.tsx: check icon
- ColumnVisibilityButton.tsx: check icon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static Icon to @coss/ui/icons (batch 16)

- EventWorkflowsTab.tsx: lock
- event-types-listing-view.tsx: search
- onboarding-migrate-members-browser-view.tsx: arrow-left, arrow-right, rotate-cw, lock, ellipsis-vertical, check, user-plus
- onboarding-migrate-teams-browser-view.tsx: arrow-left, arrow-right, rotate-cw, lock, ellipsis-vertical, check, arrow-right, users
- EventAvailabilityTab.tsx: globe, user
- onboarding-teams-browser-view.tsx: arrow-left, arrow-right, rotate-cw, lock, ellipsis-vertical, check, arrow-right, users
- FormBuilder.tsx: mail, phone, arrow-up, arrow-down
- AIEventController.tsx: info
- EventTeamAssignmentTab.tsx: x

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static Icon to @coss/ui/icons (batch 17)

- EventAdvancedTab.tsx: pencil, info
- HostLocations.tsx: triangle-alert, loader
- EventTypeLayout.tsx: loader
- Locations.tsx: x, corner-down-right, check
- EditWeightsForAllTeamMembers.tsx: upload, search, info, chevron-down
- WorkflowPage.tsx: pencil

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static Icon to @coss/ui/icons (batch 18)

- TestPhoneCallDialog.tsx: phone
- WorkflowListPage.tsx: link
- UsersTable.tsx: building, lock
- TeamList.tsx: building, paintbrush, chart-line, user-plus, users, pencil
- bookings-single-view.tsx: chevron-left, check, calendar, x, external-link

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static Icon to @coss/ui/icons (batch 19)

- TeamsListing.tsx: users, refresh-ccw, user-plus, mail, video, eye-off, info
- ConnectionInfo.tsx: clipboard
- IntercomContactForm.tsx: message-circle, loader, send

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: migrate static Icon to @coss/ui/icons in teams components

- EventTypesList.tsx: check → CheckIcon
- MemberInvitationModal.tsx: user, users, building → UserIcon, UsersIcon, BuildingIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: migrate static Icon to @coss/ui/icons in workflow components

- ImportNumberDialog.tsx: info → InfoIcon
- VoiceSelector.tsx: user → UserIcon
- IncomingCallsTab.tsx: info, phone-incoming → InfoIcon, PhoneIncomingIcon
- PhoneNumberTab.tsx: loader, phone → LoaderIcon, PhoneIcon
- VoiceSelectionDialog.tsx: pause, play, user → PauseIcon, PlayIcon, UserIcon
- WebCallDialog.tsx: loader, phone, phone-off, triangle-alert, trash, message-circle → new icons (kept Icon import for dynamic mic icon)
- WorkflowDetailsPage.tsx: zap, arrow-right → ZapIcon, ArrowRightIcon
- AddActionDialog.tsx: info → InfoIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: migrate static Icon to @coss/ui/icons in WorkflowStepContainer

- info → InfoIcon (2 instances)
- phone → PhoneIcon (2 instances)
- circle-help → CircleHelpIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: migrate static Icon to @coss/ui/icons in data-table and org components

- attributes-list-view.tsx: tags → TagsIcon
- AddFilterButton.tsx: plus, eye-off → PlusIcon, EyeOffIcon
- BaseSelectFilterOptions.tsx: check → CheckIcon
- DataTable.tsx: arrow-up, arrow-down, check, chevrons-up-down, eye-off → new icons
- booking-dry-run-success-view.tsx: check → CheckIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: migrate static Icon to @coss/ui/icons in username and routing components

- UsernameTextfield.tsx: check → CheckIcon
- PremiumTextfield.tsx: check, external-link → CheckIcon, ExternalLinkIcon (kept Icon for star with fill prop)
- Header.tsx (routing-forms): pencil, menu, waypoints → PencilIcon, MenuIcon, WaypointsIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate Icon to component icons in routing-forms pages

Batch 25: Migrate static Icon usages to @coss/ui/icons components.

Files changed:
- Forms.tsx: file-text, shuffle, chart-bar, circle-check, mail, download → FileTextIcon, ShuffleIcon, ChartBarIcon, CircleCheckIcon, MailIcon, DownloadIcon
- FormEdit.tsx: chevron-down, menu → ChevronDownIcon, MenuIcon
- IncompleteBooking.tsx: globe → GlobeIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate Icon to component icons in UI package

Batch 26: Migrate static Icon usages to @coss/ui/icons components.

Files changed:
- ProgressToast.tsx: file-down, x → FileDownIcon, XIcon
- showToast.tsx: check, info, x → CheckIcon, InfoIcon, XIcon
- InfoBadge.tsx: info → InfoIcon
- MeetingTimeInTimezones.tsx: globe → GlobeIcon
- InputError.tsx: info → InfoIcon
- ConfirmationDialogContent.tsx: circle-alert, check → CircleAlertIcon, CheckIcon
- CalendarSwitch.tsx: arrow-left, rotate-cw → ArrowLeftIcon, RotateCwIcon
- ArrowButton.tsx: arrow-up, arrow-down → ArrowUpIcon, ArrowDownIcon
- FileUploader.tsx: upload → UploadIcon (kept Icon for dynamic)
- Checkbox.tsx: check → CheckIcon
- EditableHeading.tsx: pencil → PencilIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate Icon to component icons in UI and app-store packages

Batch 27: Migrate static Icon usages to @coss/ui/icons components.

Files changed:
- MultiEmail.tsx: x → XIcon (converted width to size)
- AppListCard.tsx: circle-alert → CircleAlertIcon
- SplitButton.tsx: chevron-down → ChevronDownIcon (kept Icon for dynamic)
- Calendar.tsx: chevron-left, chevron-right → ChevronLeftIcon, ChevronRightIcon
- AppDependencyComponent.tsx: check, circle-x, arrow-right → CheckIcon, CircleXIcon, ArrowRightIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate Icon to component icons in form input components

Batch 28: Migrate static Icon usages to @coss/ui/icons components.

Files changed:
- TextField.tsx: info → InfoIcon (kept Icon for x with onClick)
- Input.tsx: eye, eye-off, search → EyeIcon, EyeOffIcon, SearchIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use @coss/ui/src/icons in packages with legacy moduleResolution

Packages like packages/ui and packages/app-store use a legacy moduleResolution
setting that doesn't support subpath exports. This changes the import path from
@coss/ui/icons to @coss/ui/src/icons for these packages.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate Icon to component icons in UI form and editor components

Migrates static Icon usages:
- MultiOptionInput: x (2x), arrow-up, arrow-down
- components (select): check
- AddVariablesDropdown: chevron-down (3x)
- ToolbarPlugin: chevron-down

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate Icon to component icons in settings and platform atoms

Migrates static Icon usages:
- SettingsLayoutAppDirClient: chevron-down (2x), chevron-right (2x), arrow-left
- platform/atoms dialog: x
- platform/atoms toast: x

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: resolve @coss/ui/icons module resolution for legacy moduleResolution

Add typesVersions to @coss/ui package.json to help TypeScript resolve
subpath exports even with legacy moduleResolution: "node" setting.

Change imports from @coss/ui/src/icons to @coss/ui/icons to avoid
double-src path issue when Next.js applies import maps.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: localize 'Select field type' fallback label in FormEdit

Address Cubic AI review feedback (confidence 9/10) by using t() for
the hardcoded 'Select field type' string to ensure proper i18n support.

Co-Authored-By: unknown <>

* fix: add typesVersions wildcards for @coss/ui subpath imports

Add lib/*, components/*, and hooks/* to typesVersions to support
legacy moduleResolution with all package subpath patterns.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: resolve missing Icon references in settings and workflows

- Add Icon import for dynamic icon usages in SettingsLayoutAppDirClient
- Replace static Icon usages with component icons (InfoIcon, PhoneIcon)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: resolve icon component type errors

- Add onClick prop support to IconProps
- Fix size prop type (use number instead of string)
- Rename local MailOpenIcon to MailOpenIconWrapper to avoid shadowing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add data-testid to icon components for test compatibility

- Add testId parameter to createIcon function
- Auto-generate data-testid for all icons (e.g., "check-icon", "search-icon")
- Remove outdated mock for @calcom/ui/components/icon in tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Update package.json

* Update package.json

* fix: migrate Icon to CalendarDaysIcon in videos-single-view.tsx

Co-Authored-By: unknown <>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-12 14:30:27 +01:00

560 lines
19 KiB
TypeScript

import { useEffect } from "react";
import type { z } from "zod";
import type {
SelectLikeComponentProps,
TextLikeComponentProps,
} from "@calcom/app-store/routing-forms/components/react-awesome-query-builder/widgets";
import Widgets from "@calcom/app-store/routing-forms/components/react-awesome-query-builder/widgets";
import PhoneInput from "@calcom/web/components/phone-input";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { fieldSchema, variantsConfigSchema, FieldType } from "@calcom/prisma/zod-utils";
import { AddressInput } from "@calcom/ui/components/address";
import { InfoBadge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import { Label, CheckboxField, EmailField, InputField, Checkbox } from "@calcom/ui/components/form";
import { RadioGroup, RadioField } from "@calcom/ui/components/radio";
import { Tooltip } from "@calcom/ui/components/tooltip";
import { XIcon } from "@coss/ui/icons";
import { ComponentForField } from "./FormBuilderField";
import { propsTypes } from "@calcom/features/form-builder/propsTypes";
import { preprocessNameFieldDataWithVariant } from "@calcom/features/form-builder/utils";
export const isValidValueProp: Record<Component["propsType"], (val: unknown) => boolean> = {
boolean: (val) => typeof val === "boolean",
multiselect: (val) => val instanceof Array && val.every((v) => typeof v === "string"),
objectiveWithInput: (val) => (typeof val === "object" && val !== null ? "value" in val : false),
select: (val) => typeof val === "string",
text: (val) => typeof val === "string",
textList: (val) => val instanceof Array && val.every((v) => typeof v === "string"),
variants: (val) => (typeof val === "object" && val !== null) || typeof val === "string",
};
type Component =
| {
propsType: "text";
factory: <TProps extends TextLikeComponentProps>(props: TProps) => JSX.Element;
}
| {
propsType: "textList";
factory: <TProps extends TextLikeComponentProps<string[]>>(props: TProps) => JSX.Element;
}
| {
propsType: "select";
factory: <TProps extends SelectLikeComponentProps>(props: TProps) => JSX.Element;
}
| {
propsType: "boolean";
factory: <TProps extends TextLikeComponentProps<boolean>>(props: TProps) => JSX.Element;
}
| {
propsType: "multiselect";
factory: <TProps extends SelectLikeComponentProps<string[]>>(props: TProps) => JSX.Element;
}
| {
// Objective type question with option having a possible input
propsType: "objectiveWithInput";
factory: <
TProps extends SelectLikeComponentProps<{
value: string;
optionValue: string;
}> & {
optionsInputs: NonNullable<z.infer<typeof fieldSchema>["optionsInputs"]>;
value: { value: string; optionValue: string };
} & {
name?: string;
required?: boolean;
translatedDefaultLabel?: string;
},
>(
props: TProps
) => JSX.Element;
}
| {
propsType: "variants";
factory: <
TProps extends Omit<TextLikeComponentProps, "value" | "setValue"> & {
variant: string | undefined;
variants: z.infer<typeof variantsConfigSchema>["variants"];
value: Record<string, string> | string | undefined;
setValue: (value: string | Record<string, string>) => void;
},
>(
props: TProps
) => JSX.Element;
};
// TODO: Share FormBuilder components across react-query-awesome-builder(for Routing Forms) widgets.
// There are certain differences b/w two. Routing Forms expect label to be provided by the widget itself and FormBuilder adds label itself and expect no label to be added by component.
// Routing Form approach is better as it provides more flexibility to show the label in complex components. But that can't be done right now because labels are missing consistent asterisk required support across different components
export const Components: Record<FieldType, Component> = {
text: {
propsType: propsTypes.text,
factory: (props) => <Widgets.TextWidget id={props.name} noLabel={true} {...props} />,
},
textarea: {
propsType: propsTypes.textarea,
// TODO: Make rows configurable in the form builder
factory: (props) => <Widgets.TextAreaWidget id={props.name} rows={3} {...props} />,
},
number: {
propsType: propsTypes.number,
factory: (props) => <Widgets.NumberWidget id={props.name} noLabel={true} {...props} />,
},
name: {
propsType: propsTypes.name,
// Keep special "name" type field and later build split(FirstName and LastName) variant of it.
factory: (props) => {
const { variant: variantName = "fullName" } = props;
const onChange = (name: string, value: string) => {
let currentValue = props.value;
if (typeof currentValue !== "object") {
currentValue = {};
}
props.setValue({
...currentValue,
[name]: value,
});
};
if (!props.variants) {
throw new Error("'variants' is required for 'name' type of field");
}
if (variantName !== "firstAndLastName" && variantName !== "fullName") {
throw new Error(`Invalid variant name '${variantName}' for 'name' type of field`);
}
const value = preprocessNameFieldDataWithVariant(variantName, props.value);
if (variantName === "fullName") {
if (typeof value !== "string") {
throw new Error("Invalid value for 'fullName' variant");
}
const variant = props.variants[variantName];
const variantField = variant.fields[0];
return (
<InputField
name="name"
showAsteriskIndicator={true}
placeholder={variantField.placeholder}
label={variantField.label}
containerClassName="w-full"
readOnly={props.readOnly}
value={value}
required={variantField.required}
type="text"
autoComplete="name"
onChange={(e) => {
props.setValue(e.target.value);
}}
/>
);
}
const variant = props.variants[variantName];
if (typeof value !== "object") {
throw new Error("Invalid value for 'fullName' variant");
}
return (
<div className="flex space-x-4">
{variant.fields.map((variantField) => (
<InputField
// Because the container is flex(and thus margin is being computed towards container height), I need to explicitly ensure that margin-bottom for the input becomes 0, which is mb-2 otherwise
className="mb-0!"
showAsteriskIndicator={true}
key={variantField.name}
name={variantField.name}
readOnly={props.readOnly}
placeholder={variantField.placeholder}
label={variantField.label}
containerClassName={`w-full testid-${variantField.name}`}
value={value[variantField.name as keyof typeof value]}
required={variantField.required}
type="text"
autoComplete={
variantField.name === "firstName"
? "given-name"
: variantField.name === "lastName"
? "family-name"
: undefined
}
onChange={(e) => onChange(variantField.name, e.target.value)}
/>
))}
</div>
);
},
},
phone: {
propsType: propsTypes.phone,
factory: ({ setValue, readOnly, ...props }) => {
if (!props) {
return <div />;
}
return (
<PhoneInput
disabled={readOnly}
onChange={(val: string) => {
setValue(val);
}}
{...props}
/>
);
},
},
email: {
propsType: propsTypes.email,
factory: (props) => {
if (!props) {
return <div />;
}
return (
<InputField
type="email"
id={props.name}
noLabel={true}
autoComplete="email"
{...props}
onChange={(e) => props.setValue(e.target.value)}
/>
);
},
},
address: {
propsType: propsTypes.address,
factory: (props) => {
return (
<AddressInput
id={props.name}
onChange={(val) => {
props.setValue(val);
}}
{...props}
disabled={props.readOnly}
/>
);
},
},
multiemail: {
propsType: propsTypes.multiemail,
//TODO: Make it a ui component
factory: function MultiEmail({ value, readOnly, label, setValue, ...props }) {
const placeholder = props.placeholder;
const { t } = useLocale();
value = value || [];
return (
<>
{value.length ? (
<div>
<label htmlFor="guests" className="text-default mb-1 block text-sm font-medium">
{label}
</label>
<ul>
{value.map((field, index) => (
<li key={index}>
<EmailField
id={`${props.name}.${index}`}
disabled={readOnly}
value={value[index]}
onChange={(e) => {
value[index] = e.target.value.toLowerCase();
setValue(value);
}}
placeholder={placeholder}
label={<></>}
required
onClickAddon={() => {
value.splice(index, 1);
setValue(value);
}}
addOnSuffix={
!readOnly ? (
<Tooltip content="Remove email">
<button className="m-1" type="button">
<XIcon size={12} className="text-default" />
</button>
</Tooltip>
) : null
}
/>
</li>
))}
</ul>
{!readOnly && (
<Button
data-testid="add-another-guest"
type="button"
color="minimal"
StartIcon="user-plus"
className="my-2.5"
onClick={() => {
value.push("");
setValue(value);
}}>
{t("add_another")}
</Button>
)}
</div>
) : (
<></>
)}
{!value.length && !readOnly && (
<Button
data-testid="add-guests"
color="minimal"
variant="button"
StartIcon="user-plus"
onClick={() => {
value.push("");
setValue(value);
}}
className="mr-auto h-fit whitespace-normal text-left">
<span className="flex-1">{label}</span>
</Button>
)}
</>
);
},
},
multiselect: {
propsType: propsTypes.multiselect,
factory: (props) => {
const newProps = {
...props,
listValues: props.options.map((o) => ({ title: o.label, value: o.value })),
};
return <Widgets.MultiSelectWidget id={props.name} {...newProps} />;
},
},
select: {
propsType: propsTypes.select,
factory: (props) => {
const newProps = {
...props,
listValues: props.options.map((o) => ({ title: o.label, value: o.value })),
};
return <Widgets.SelectWidget id={props.name} {...newProps} />;
},
},
checkbox: {
propsType: propsTypes.checkbox,
factory: ({ options, readOnly, setValue, value }) => {
value = value || [];
return (
<div>
{options.map((option, i) => {
return (
<label key={i} className="block">
<Checkbox
disabled={readOnly}
onCheckedChange={(checked) => {
const newValue = value.filter((v) => v !== option.value);
if (checked) {
newValue.push(option.value);
}
setValue(newValue);
}}
value={option.value}
checked={value.includes(option.value)}
/>
<span className="text-emphasis me-2 ms-2 text-sm">{option.label ?? ""}</span>
</label>
);
})}
</div>
);
},
},
radio: {
propsType: propsTypes.radio,
factory: ({ setValue, name, value, options, readOnly }) => {
return (
<RadioGroup
disabled={readOnly}
value={value}
onValueChange={(e) => {
setValue(e);
}}>
<>
{options.map((option, i) => (
<RadioField
label={option.label}
key={`option.${i}.radio`}
value={option.label}
id={`${name}.option.${i}.radio`}
/>
))}
</>
</RadioGroup>
);
},
},
radioInput: {
propsType: propsTypes.radioInput,
factory: function RadioInputWithLabel({
name,
label,
options,
optionsInputs,
value,
setValue,
readOnly,
translatedDefaultLabel,
}) {
useEffect(() => {
if (!value) {
setValue({
value: options[0]?.value,
optionValue: "",
});
}
}, [options, setValue, value]);
const { t } = useLocale();
const didUserProvideLabel = (
label: string | undefined,
translatedDefaultLabel: string | undefined
): label is string => {
return label && translatedDefaultLabel ? translatedDefaultLabel !== label : false;
};
const getCleanLabel = (label: string): string | JSX.Element => {
if (!label) {
return "";
}
return label.search(/^https?:\/\//) !== -1 ? (
<a href={label} target="_blank">
<span className="underline">{label}</span>
</a>
) : (
label
);
};
return (
<div>
<div>
<div className="mb-2">
{options.length > 1 ? (
options.map((option, i) => {
return (
<label key={i} className="mb-1 flex items-center">
<input
type="radio"
disabled={readOnly}
name={name}
className="bg-default after:bg-default border-emphasis hover:bg-subtle hover:after:bg-subtle checked:bg-brand-default checked:hover:bg-brand-default checked:after:bg-default dark:checked:bg-brand-default dark:checked:after:bg-brand-accent dark:hover:bg-subtle dark:checked:hover:bg-brand-default text-(--cal-brand) flex h-4 w-4 cursor-pointer items-center justify-center transition after:h-[6px] after:w-[6px] after:rounded-full after:content-[''] after:hover:block focus:ring-0 focus:ring-offset-0 focus:outline-none ltr:mr-2 rtl:ml-2"
value={option.value}
onChange={(e) => {
setValue({
value: e.target.value,
optionValue: "",
});
}}
checked={value?.value === option.value}
/>
<span className="text-emphasis me-2 ms-2 text-sm">
{option.value === "somewhereElse"
? t("somewhere_else")
: (getCleanLabel(option.label) ?? "")}
</span>
<span>
{option.value === "phone" && (
<InfoBadge content={t("number_in_international_format")} />
)}
</span>
</label>
);
})
) : (
// Use the only option itself to determine if the field is required or not.
<>
<Label className="flex items-center">
{/* We still want to show the label of the field if it is changed by the user otherwise the best label would be the option label */}
{options[0].value === "somewhereElse"
? translatedDefaultLabel
: getCleanLabel(
didUserProvideLabel(label, translatedDefaultLabel) ? label : options[0].label
)}
{!readOnly && optionsInputs[options[0].value]?.required ? (
<span className="text-default -mb-2 ml-1 text-sm font-medium">*</span>
) : null}
{options[0].value === "phone" && (
<InfoBadge content={t("number_in_international_format")} />
)}
</Label>
</>
)}
</div>
</div>
{(() => {
const optionField = optionsInputs[value?.value];
if (!optionField) {
return null;
}
return (
<div>
<ComponentForField
readOnly={!!readOnly}
field={{
...optionField,
name: "optionField",
}}
value={value?.optionValue}
setValue={(val: string | undefined) => {
setValue({
value: value?.value,
optionValue: val || "",
});
}}
/>
</div>
);
})()}
</div>
);
},
},
boolean: {
propsType: propsTypes.boolean,
factory: ({ readOnly, name, label, value, setValue }) => {
return (
<div className="flex">
<CheckboxField
name={name}
onChange={(e) => {
if (e.target.checked) {
setValue(true);
} else {
setValue(false);
}
}}
placeholder=""
checked={value}
disabled={readOnly}
description=""
// Form Builder ensures that it would be safe HTML in here if the field type supports it. So, we can safely use label value in `descriptionAsSafeHtml`
descriptionAsSafeHtml={label ?? ""}
/>
</div>
);
},
},
url: {
propsType: propsTypes.url,
factory: (props) => {
return <Widgets.TextWidget type="url" autoComplete="url" noLabel={true} {...props} />;
},
},
} as const;
// Should use `satisfies` to check if the `type` is from supported types. But satisfies doesn't work with Next.js config