Skip to content

Commit b9366bc

Browse files
authored
feat: Add support for checkbox, toggle, text input elements to script form (#3219)
1 parent 63a5831 commit b9366bc

4 files changed

Lines changed: 358 additions & 4 deletions

File tree

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1559,11 +1559,52 @@ A drop-down to select a single item from a list.
15591559
| Parameter | Value | Optional | Description |
15601560
|-----------|--------|----------|----------------------------------|
15611561
| `element` | `String` | No | Must be `"dropDown"`. |
1562+
| `name` | `String` | No | The key used in `formData`. |
15621563
| `label` | `String` | No | The display label shown next to the dropdown. |
1564+
| `description` | `String` | Yes | Secondary text below the label. |
15631565
| `items` | `Array` | No | The selectable options. |
15641566
| `items[].title` | `String` | No | The display text of the option. |
15651567
| `items[].value` | `String` | No | The value of the option. |
15661568
1569+
###### Checkbox
1570+
1571+
A checkbox for boolean input.
1572+
1573+
| Parameter | Value | Optional | Default | Description |
1574+
|-----------|--------|----------|---------|----------------------------------|
1575+
| `element` | `String` | No | - | Must be `"checkbox"`. |
1576+
| `name` | `String` | No | - | The key used in `formData`. |
1577+
| `label` | `String` | No | - | The display label. |
1578+
| `description` | `String` | Yes | - | Secondary text below the label. |
1579+
| `default` | `Boolean`| Yes | `false` | The initial checked state. |
1580+
1581+
###### Toggle
1582+
1583+
A toggle switch for boolean input.
1584+
1585+
| Parameter | Value | Optional | Default | Description |
1586+
|-------------|----------|----------|---------|----------------------------------|
1587+
| `element` | `String` | No | - | Must be `"toggle"`. |
1588+
| `name` | `String` | No | - | The key used in `formData`. |
1589+
| `label` | `String` | No | - | The display label. |
1590+
| `description` | `String` | Yes | - | Secondary text below the label. |
1591+
| `default` | `Boolean`| Yes | `false` | The initial toggle state. |
1592+
| `labelTrue` | `String` | Yes | `"Yes"` | Label for the `true` side. |
1593+
| `labelFalse` | `String` | Yes | `"No"` | Label for the `false` side. |
1594+
1595+
###### Text Input
1596+
1597+
A single-line text input.
1598+
1599+
| Parameter | Value | Optional | Default | Description |
1600+
|-------------|----------|----------|---------|----------------------------------|
1601+
| `element` | `String` | No | - | Must be `"textInput"`. |
1602+
| `name` | `String` | No | - | The key used in `formData`. |
1603+
| `label` | `String` | No | - | The display label. |
1604+
| `description` | `String` | Yes | - | Secondary text below the label. |
1605+
| `placeholder` | `String` | Yes | `""` | Placeholder text. |
1606+
| `default` | `String` | Yes | `""` | The initial value. |
1607+
15671608
### Graph
15681609
15691610
▶️ *Core > Browser > Graph*

src/dashboard/Data/Browser/ScriptResponseModal.react.js

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import Field from 'components/Field/Field.react';
1111
import Label from 'components/Label/Label.react';
1212
import Dropdown from 'components/Dropdown/Dropdown.react';
1313
import Option from 'components/Dropdown/Option.react';
14+
import Checkbox from 'components/Checkbox/Checkbox.react';
15+
import Toggle from 'components/Toggle/Toggle.react';
16+
import TextInput from 'components/TextInput/TextInput.react';
1417

1518
export default class ScriptResponseModal extends React.Component {
1619
constructor(props) {
@@ -21,6 +24,12 @@ export default class ScriptResponseModal extends React.Component {
2124
const key = element.name || String(index);
2225
if (element.element === 'dropDown' && element.items?.length > 0) {
2326
formData[key] = element.items[0].value;
27+
} else if (element.element === 'checkbox') {
28+
formData[key] = element.default ?? false;
29+
} else if (element.element === 'toggle') {
30+
formData[key] = element.default ?? false;
31+
} else if (element.element === 'textInput') {
32+
formData[key] = element.default ?? '';
2433
}
2534
});
2635

@@ -39,7 +48,7 @@ export default class ScriptResponseModal extends React.Component {
3948
return (
4049
<Field
4150
key={key}
42-
label={<Label text={element.label || element.name || 'Select'} />}
51+
label={<Label text={element.label || element.name || 'Select'} description={element.description} />}
4352
input={
4453
<Dropdown
4554
fixed={true}
@@ -61,6 +70,79 @@ export default class ScriptResponseModal extends React.Component {
6170
);
6271
}
6372

73+
if (element.element === 'checkbox') {
74+
return (
75+
<Field
76+
key={key}
77+
label={<Label text={element.label || element.name || 'Checkbox'} description={element.description} />}
78+
input={
79+
<Checkbox
80+
label=""
81+
checked={this.state.formData[key]}
82+
onChange={value =>
83+
this.setState(prev => ({
84+
formData: { ...prev.formData, [key]: value },
85+
}))
86+
}
87+
/>
88+
}
89+
/>
90+
);
91+
}
92+
93+
if (element.element === 'toggle') {
94+
const hasCustomLabels = element.labelTrue || element.labelFalse;
95+
const toggleProps = hasCustomLabels
96+
? {
97+
type: Toggle.Types.TWO_WAY,
98+
optionLeft: element.labelFalse || 'No',
99+
optionRight: element.labelTrue || 'Yes',
100+
value: this.state.formData[key]
101+
? (element.labelTrue || 'Yes')
102+
: (element.labelFalse || 'No'),
103+
onChange: value =>
104+
this.setState(prev => ({
105+
formData: { ...prev.formData, [key]: value === (element.labelTrue || 'Yes') },
106+
})),
107+
}
108+
: {
109+
type: Toggle.Types.YES_NO,
110+
value: this.state.formData[key],
111+
onChange: value =>
112+
this.setState(prev => ({
113+
formData: { ...prev.formData, [key]: value },
114+
})),
115+
};
116+
117+
return (
118+
<Field
119+
key={key}
120+
label={<Label text={element.label || element.name || 'Toggle'} description={element.description} />}
121+
input={<Toggle {...toggleProps} />}
122+
/>
123+
);
124+
}
125+
126+
if (element.element === 'textInput') {
127+
return (
128+
<Field
129+
key={key}
130+
label={<Label text={element.label || element.name || 'Text'} description={element.description} />}
131+
input={
132+
<TextInput
133+
placeholder={element.placeholder || ''}
134+
value={this.state.formData[key]}
135+
onChange={value =>
136+
this.setState(prev => ({
137+
formData: { ...prev.formData, [key]: value },
138+
}))
139+
}
140+
/>
141+
}
142+
/>
143+
);
144+
}
145+
64146
return null;
65147
}
66148

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
jest.dontMock('../../dashboard/Data/Browser/ScriptResponseModal.react');
5+
jest.dontMock('../../components/Field/Field.react');
6+
jest.dontMock('../../components/Label/Label.react');
7+
jest.dontMock('../../components/Checkbox/Checkbox.react');
8+
jest.dontMock('../../components/Toggle/Toggle.react');
9+
jest.dontMock('../../components/TextInput/TextInput.react');
10+
jest.dontMock('../../components/Dropdown/Dropdown.react');
11+
jest.dontMock('../../components/Dropdown/Option.react');
12+
jest.dontMock('../../components/Modal/Modal.react');
13+
jest.dontMock('../../components/Button/Button.react');
14+
jest.dontMock('../../components/Icon/Icon.react');
15+
jest.dontMock('../Position');
16+
17+
// Mock Popover to avoid createPortal issues with react-test-renderer
18+
jest.mock('../../components/Popover/Popover.react', () => {
19+
const React = require('react');
20+
return class Popover extends React.Component {
21+
render() {
22+
return <div>{this.props.children}</div>;
23+
}
24+
};
25+
});
26+
27+
import React from 'react';
28+
import renderer from 'react-test-renderer';
29+
const ScriptResponseModal = require('../../dashboard/Data/Browser/ScriptResponseModal.react').default;
30+
31+
const defaultProps = {
32+
objectIds: ['obj1'],
33+
onCancel: jest.fn(),
34+
onConfirm: jest.fn(),
35+
};
36+
37+
describe('ScriptResponseModal', () => {
38+
describe('checkbox element', () => {
39+
it('initializes with default false', () => {
40+
const form = {
41+
elements: [{ element: 'checkbox', name: 'confirmed', label: 'Confirm' }],
42+
};
43+
const component = renderer.create(<ScriptResponseModal form={form} {...defaultProps} />);
44+
const instance = component.getInstance();
45+
expect(instance.state.formData.confirmed).toBe(false);
46+
});
47+
48+
it('initializes with custom default', () => {
49+
const form = {
50+
elements: [{ element: 'checkbox', name: 'confirmed', label: 'Confirm', default: true }],
51+
};
52+
const component = renderer.create(<ScriptResponseModal form={form} {...defaultProps} />);
53+
const instance = component.getInstance();
54+
expect(instance.state.formData.confirmed).toBe(true);
55+
});
56+
57+
it('renders with description', () => {
58+
const form = {
59+
elements: [{
60+
element: 'checkbox',
61+
name: 'confirmed',
62+
label: 'Confirm',
63+
description: 'Please confirm',
64+
}],
65+
};
66+
const tree = renderer.create(<ScriptResponseModal form={form} {...defaultProps} />).toJSON();
67+
expect(JSON.stringify(tree)).toContain('Please confirm');
68+
});
69+
});
70+
71+
describe('toggle element', () => {
72+
it('initializes with default false', () => {
73+
const form = {
74+
elements: [{ element: 'toggle', name: 'enabled', label: 'Enable' }],
75+
};
76+
const component = renderer.create(<ScriptResponseModal form={form} {...defaultProps} />);
77+
const instance = component.getInstance();
78+
expect(instance.state.formData.enabled).toBe(false);
79+
});
80+
81+
it('initializes with custom default true', () => {
82+
const form = {
83+
elements: [{ element: 'toggle', name: 'enabled', label: 'Enable', default: true }],
84+
};
85+
const component = renderer.create(<ScriptResponseModal form={form} {...defaultProps} />);
86+
const instance = component.getInstance();
87+
expect(instance.state.formData.enabled).toBe(true);
88+
});
89+
90+
it('renders with description', () => {
91+
const form = {
92+
elements: [{
93+
element: 'toggle',
94+
name: 'enabled',
95+
label: 'Enable',
96+
description: 'Toggle this feature',
97+
}],
98+
};
99+
const tree = renderer.create(<ScriptResponseModal form={form} {...defaultProps} />).toJSON();
100+
expect(JSON.stringify(tree)).toContain('Toggle this feature');
101+
});
102+
103+
it('renders with custom labels', () => {
104+
const form = {
105+
elements: [{
106+
element: 'toggle',
107+
name: 'enabled',
108+
label: 'Enable',
109+
labelTrue: 'Enabled',
110+
labelFalse: 'Disabled',
111+
}],
112+
};
113+
const tree = renderer.create(<ScriptResponseModal form={form} {...defaultProps} />).toJSON();
114+
const json = JSON.stringify(tree);
115+
expect(json).toContain('Enabled');
116+
expect(json).toContain('Disabled');
117+
});
118+
});
119+
120+
describe('textInput element', () => {
121+
it('initializes with default empty string', () => {
122+
const form = {
123+
elements: [{ element: 'textInput', name: 'reason', label: 'Reason' }],
124+
};
125+
const component = renderer.create(<ScriptResponseModal form={form} {...defaultProps} />);
126+
const instance = component.getInstance();
127+
expect(instance.state.formData.reason).toBe('');
128+
});
129+
130+
it('initializes with custom default', () => {
131+
const form = {
132+
elements: [{ element: 'textInput', name: 'reason', label: 'Reason', default: 'N/A' }],
133+
};
134+
const component = renderer.create(<ScriptResponseModal form={form} {...defaultProps} />);
135+
const instance = component.getInstance();
136+
expect(instance.state.formData.reason).toBe('N/A');
137+
});
138+
139+
it('renders with placeholder', () => {
140+
const form = {
141+
elements: [{
142+
element: 'textInput',
143+
name: 'reason',
144+
label: 'Reason',
145+
placeholder: 'Enter reason...',
146+
}],
147+
};
148+
const tree = renderer.create(<ScriptResponseModal form={form} {...defaultProps} />).toJSON();
149+
expect(JSON.stringify(tree)).toContain('Enter reason...');
150+
});
151+
152+
it('renders with description', () => {
153+
const form = {
154+
elements: [{
155+
element: 'textInput',
156+
name: 'reason',
157+
label: 'Reason',
158+
description: 'Provide a reason',
159+
}],
160+
};
161+
const tree = renderer.create(<ScriptResponseModal form={form} {...defaultProps} />).toJSON();
162+
expect(JSON.stringify(tree)).toContain('Provide a reason');
163+
});
164+
});
165+
166+
describe('dropDown element (existing)', () => {
167+
it('initializes with first item value', () => {
168+
const form = {
169+
elements: [{
170+
element: 'dropDown',
171+
name: 'role',
172+
label: 'Role',
173+
items: [{ title: 'Admin', value: 'admin' }, { title: 'User', value: 'user' }],
174+
}],
175+
};
176+
const component = renderer.create(<ScriptResponseModal form={form} {...defaultProps} />);
177+
const instance = component.getInstance();
178+
expect(instance.state.formData.role).toBe('admin');
179+
});
180+
});
181+
182+
describe('mixed elements', () => {
183+
it('initializes all element types correctly', () => {
184+
const form = {
185+
elements: [
186+
{ element: 'dropDown', name: 'role', items: [{ title: 'Admin', value: 'admin' }] },
187+
{ element: 'checkbox', name: 'confirmed', default: true },
188+
{ element: 'toggle', name: 'enabled' },
189+
{ element: 'textInput', name: 'reason', default: 'test' },
190+
],
191+
};
192+
const component = renderer.create(<ScriptResponseModal form={form} {...defaultProps} />);
193+
const instance = component.getInstance();
194+
expect(instance.state.formData).toEqual({
195+
role: 'admin',
196+
confirmed: true,
197+
enabled: false,
198+
reason: 'test',
199+
});
200+
});
201+
});
202+
203+
describe('form submission', () => {
204+
it('calls onConfirm with formData', () => {
205+
const onConfirm = jest.fn();
206+
const form = {
207+
elements: [
208+
{ element: 'checkbox', name: 'confirmed', default: true },
209+
{ element: 'textInput', name: 'reason', default: 'hello' },
210+
],
211+
};
212+
const component = renderer.create(
213+
<ScriptResponseModal form={form} objectIds={['obj1']} onCancel={jest.fn()} onConfirm={onConfirm} />
214+
);
215+
const instance = component.getInstance();
216+
instance.handleConfirm();
217+
expect(onConfirm).toHaveBeenCalledWith({ confirmed: true, reason: 'hello' });
218+
});
219+
});
220+
});

0 commit comments

Comments
 (0)