Skip to content

Commit fcbedf7

Browse files
authored
Merge pull request wso2#10165 from wso2/sync-pr-10159-to-next
[Sync][master -> next][wso2#10159]: Add radio button support in flow composer
2 parents 6c10cb0 + a0c05dc commit fcbedf7

10 files changed

Lines changed: 297 additions & 16 deletions

File tree

.changeset/good-elephants-itch.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@wso2is/admin.flow-builder-core.v1": patch
3+
"@wso2is/identity-apps-core": patch
4+
"@wso2is/console": patch
5+
---
6+
7+
Add radio button support in flow composer
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/**
2+
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
import Box from "@oxygen-ui/react/Box";
20+
import Button from "@oxygen-ui/react/Button";
21+
import IconButton from "@oxygen-ui/react/IconButton";
22+
import TextField from "@oxygen-ui/react/TextField";
23+
import Tooltip from "@oxygen-ui/react/Tooltip";
24+
import Typography from "@oxygen-ui/react/Typography";
25+
import { PlusIcon, TrashIcon } from "@oxygen-ui/react-icons";
26+
import { IdentifiableComponentInterface } from "@wso2is/core/models";
27+
import React, { ChangeEvent, FunctionComponent, ReactElement } from "react";
28+
import { FieldOption } from "../../models/base";
29+
import { Resource } from "../../models/resources";
30+
31+
/**
32+
* Props interface of {@link ChoiceOptionsPropertyField}
33+
*/
34+
export interface ChoiceOptionsPropertyFieldPropsInterface extends IdentifiableComponentInterface {
35+
/**
36+
* The resource associated with the property.
37+
*/
38+
resource: Resource;
39+
/**
40+
* The list of options to render.
41+
*/
42+
options: FieldOption[];
43+
/**
44+
* The event handler for the property change.
45+
* @param propertyKey - The key of the property.
46+
* @param newValue - The new value of the property.
47+
* @param resource - The resource associated with the property.
48+
*/
49+
onChange: (propertyKey: string, newValue: any, resource: Resource) => void;
50+
}
51+
52+
/**
53+
* Property field component that renders an editable list of radio button options.
54+
* Each option exposes a Label and a Value field. Options can be added and removed.
55+
*
56+
* @param props - Props injected to the component.
57+
* @returns The ChoiceOptionsPropertyField component.
58+
*/
59+
const ChoiceOptionsPropertyField: FunctionComponent<ChoiceOptionsPropertyFieldPropsInterface> = ({
60+
"data-componentid": componentId = "choice-options-property-field",
61+
resource,
62+
options,
63+
onChange
64+
}: ChoiceOptionsPropertyFieldPropsInterface): ReactElement => {
65+
66+
const handleOptionChange = (index: number, field: "label" | "value", newValue: string): void => {
67+
const updatedOptions: FieldOption[] = options.map((option: FieldOption, i: number) => {
68+
if (i === index) {
69+
// Keep key in sync with value so that keys remain unique and meaningful.
70+
return field === "value"
71+
? { ...option, key: newValue, value: newValue }
72+
: { ...option, label: newValue };
73+
}
74+
75+
return option;
76+
});
77+
78+
onChange("config.options", updatedOptions, resource);
79+
};
80+
81+
const handleAddOption = (): void => {
82+
const newIndex: number = options.length + 1;
83+
const newValue: string = `option${newIndex}`;
84+
const newOption: FieldOption = {
85+
key: newValue,
86+
label: `Option ${newIndex}`,
87+
value: newValue
88+
};
89+
90+
onChange("config.options", [ ...options, newOption ], resource);
91+
};
92+
93+
const handleRemoveOption = (index: number): void => {
94+
const updatedOptions: FieldOption[] = options.filter((_: FieldOption, i: number) => i !== index);
95+
96+
onChange("config.options", updatedOptions, resource);
97+
};
98+
99+
return (
100+
<Box data-componentid={ componentId }>
101+
<Typography variant="caption" color="textSecondary" sx={ { display: "block", mb: 0.5 } }>
102+
Options
103+
</Typography>
104+
{ options.map((option: FieldOption, index: number) => (
105+
<Box
106+
key={ index }
107+
sx={ { alignItems: "center", display: "flex", gap: 1, mb: 1 } }
108+
data-componentid={ `${componentId}-option-${index}` }
109+
>
110+
<TextField
111+
fullWidth
112+
size="small"
113+
label="Label"
114+
defaultValue={ option.label }
115+
onChange={ (e: ChangeEvent<HTMLInputElement>) =>
116+
handleOptionChange(index, "label", e.target.value)
117+
}
118+
data-componentid={ `${componentId}-option-${index}-label` }
119+
/>
120+
<TextField
121+
fullWidth
122+
size="small"
123+
label="Value"
124+
defaultValue={ option.value }
125+
onChange={ (e: ChangeEvent<HTMLInputElement>) =>
126+
handleOptionChange(index, "value", e.target.value)
127+
}
128+
data-componentid={ `${componentId}-option-${index}-value` }
129+
/>
130+
<Tooltip title="Remove option">
131+
<span>
132+
<IconButton
133+
size="small"
134+
onClick={ () => handleRemoveOption(index) }
135+
disabled={ options.length <= 1 }
136+
aria-label="Remove option"
137+
data-componentid={ `${componentId}-option-${index}-remove` }
138+
>
139+
<TrashIcon size={ 14 } />
140+
</IconButton>
141+
</span>
142+
</Tooltip>
143+
</Box>
144+
)) }
145+
<Button
146+
size="small"
147+
variant="text"
148+
startIcon={ <PlusIcon size={ 14 } /> }
149+
onClick={ handleAddOption }
150+
data-componentid={ `${componentId}-add-option` }
151+
>
152+
Add option
153+
</Button>
154+
</Box>
155+
);
156+
};
157+
158+
export default ChoiceOptionsPropertyField;

features/admin.flow-builder-core.v1/components/resource-property-panel/common-element-property-factory.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
2+
* Copyright (c) 2025-2026, WSO2 LLC. (https://www.wso2.com).
33
*
44
* WSO2 LLC. licenses this file to you under the Apache License,
55
* Version 2.0 (the "License"); you may not use this file except
@@ -20,10 +20,12 @@ import TextField from "@oxygen-ui/react/TextField";
2020
import { IdentifiableComponentInterface } from "@wso2is/core/models";
2121
import React, { FunctionComponent, ReactElement } from "react";
2222
import CheckboxPropertyField from "./checkbox-property-field";
23+
import ChoiceOptionsPropertyField from "./choice-options-property-field";
2324
import RichTextWithTranslation from "./rich-text/rich-text-with-translation";
2425
import TextPropertyField from "./text-property-field";
2526
import FlowBuilderElementConstants from "../../constants/flow-builder-element-constants";
26-
import { ElementTypes } from "../../models/elements";
27+
import { FieldOption } from "../../models/base";
28+
import { ElementTypes, InputVariants } from "../../models/elements";
2729
import { Resource } from "../../models/resources";
2830

2931
/**
@@ -69,6 +71,19 @@ const CommonElementPropertyFactory: FunctionComponent<CommonElementPropertyFacto
6971
onChange,
7072
...rest
7173
}: CommonElementPropertyFactoryPropsInterface): ReactElement | null => {
74+
if ((resource.type === ElementTypes.Choice ||
75+
(resource.type === ElementTypes.Input && resource.variant === InputVariants.Choice)) &&
76+
propertyKey === "options") {
77+
return (
78+
<ChoiceOptionsPropertyField
79+
resource={ resource }
80+
options={ (propertyValue as FieldOption[]) ?? [] }
81+
onChange={ onChange }
82+
data-componentid={ `${componentId}-${propertyKey}` }
83+
/>
84+
);
85+
}
86+
7287
if (propertyKey === "text") {
7388
if (resource.type === ElementTypes.RichText) {
7489
return (

features/admin.flow-builder-core.v1/components/resources/elements/common-element-factory.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
2+
* Copyright (c) 2025-2026, WSO2 LLC. (https://www.wso2.com).
33
*
44
* WSO2 LLC. licenses this file to you under the Apache License,
55
* Version 2.0 (the "License"); you may not use this file except
@@ -77,6 +77,10 @@ const CommonElementFactory: FunctionComponent<CommonElementFactoryPropsInterface
7777
return <CheckboxAdapter stepId={ stepId } resource={ resource } />;
7878
}
7979

80+
if (resource.variant === InputVariants.Choice) {
81+
return <ChoiceAdapter stepId={ stepId } resource={ resource } />;
82+
}
83+
8084
if (resource.variant === InputVariants.Telephone) {
8185
return <PhoneNumberInputAdapter stepId={ stepId } resource={ resource } />;
8286
}

features/admin.flow-builder-core.v1/constants/visual-flow-constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
2+
* Copyright (c) 2025-2026, WSO2 LLC. (https://www.wso2.com).
33
*
44
* WSO2 LLC. licenses this file to you under the Apache License,
55
* Version 2.0 (the "License"); you may not use this file except
@@ -77,6 +77,7 @@ class VisualFlowConstants {
7777
];
7878

7979
public static readonly FLOW_BUILDER_FORM_ALLOWED_RESOURCE_TYPES: string[] = [
80+
ElementTypes.Choice,
8081
ElementTypes.Input,
8182
ElementTypes.Button,
8283
ElementTypes.Typography,

features/admin.flow-builder-core.v1/data/elements.json

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,13 @@
185185
"type": "INPUT",
186186
"version": "0.1.0",
187187
"deprecated": false,
188+
"variant": "CHOICE",
188189
"display": {
189-
"label": "Choice",
190+
"label": "Radio Button",
191+
"displayName": "Choice",
190192
"image": "assets/images/icons/boolean.svg",
191-
"showOnResourcePanel": false
193+
"showOnResourcePanel": true
192194
},
193-
"variant": "DROPDOWN",
194195
"config": {
195196
"hint": "",
196197
"label": "Choice",
@@ -206,11 +207,6 @@
206207
"label": "Option 2",
207208
"key": "option2",
208209
"value": "option2"
209-
},
210-
{
211-
"label": "Option 3",
212-
"key": "option3",
213-
"value": "option3"
214210
}
215211
]
216212
}

features/admin.flow-builder-core.v1/models/elements.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
2+
* Copyright (c) 2025-2026, WSO2 LLC. (https://www.wso2.com).
33
*
44
* WSO2 LLC. licenses this file to you under the Apache License,
55
* Version 2.0 (the "License"); you may not use this file except
@@ -70,7 +70,8 @@ export enum InputVariants {
7070
Telephone = "TELEPHONE",
7171
Number = "NUMBER",
7272
Checkbox = "CHECKBOX",
73-
OTP = "OTP"
73+
OTP = "OTP",
74+
Choice = "CHOICE"
7475
}
7576

7677
export enum ButtonVariants {

identity-apps-core/react-ui-core/src/components/adapters/input-field-adapter.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
2+
* Copyright (c) 2025-2026, WSO2 LLC. (https://www.wso2.com).
33
*
44
* WSO2 LLC. licenses this file to you under the Apache License,
55
* Version 2.0 (the "License"); you may not use this file except
@@ -25,6 +25,7 @@ import NumberFieldAdapter from "./number-field-adapter";
2525
import OTPFieldAdapter from "./otp-field-adapter";
2626
import PasswordFieldAdapter from "./password-field-adapter";
2727
import PhoneNumberFieldAdapter from "./phone-number-field-adapter";
28+
import RadioFieldAdapter from "./radio-field-adapter";
2829
import TextFieldAdapter from "./text-field-adapter";
2930

3031
const InputFieldAdapter = ({ component, formState, formStateHandler, formFieldError }) => {
@@ -110,6 +111,15 @@ const InputFieldAdapter = ({ component, formState, formStateHandler, formFieldEr
110111
fieldErrorHandler={ formFieldError }
111112
/>
112113
);
114+
case "CHOICE":
115+
return (
116+
<RadioFieldAdapter
117+
component={ component }
118+
formState={ formState }
119+
formStateHandler={ formStateHandler }
120+
fieldErrorHandler={ formFieldError }
121+
/>
122+
);
113123
default:
114124
return null;
115125
}

0 commit comments

Comments
 (0)