Skip to content

Commit f235476

Browse files
authored
Merge pull request #1086 from rvsia/selectAll
Select all
2 parents d0c9263 + d01deba commit f235476

File tree

9 files changed

+299
-41
lines changed

9 files changed

+299
-41
lines changed

packages/carbon-component-mapper/src/select/select.js

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,7 @@ import fnToString from '@data-driven-forms/common/utils/fn-to-string';
88
import { Select as CarbonSelect, MultiSelect, SelectItem, ComboBox } from 'carbon-components-react';
99
import prepareProps from '../prepare-props';
1010

11-
export const multiOnChange = (input, simpleValue) => ({ selectedItem, selectedItems }) => {
12-
if (simpleValue) {
13-
return input.onChange(selectedItems?.map(({ value }) => value) || selectedItem.value);
14-
} else {
15-
return input.onChange(selectedItems || selectedItem);
16-
}
17-
};
11+
const onChangeWrapper = (onChange) => ({ selectedItem, selectedItems }) => onChange(selectedItems || selectedItem);
1812

1913
export const getMultiValue = (value, options) =>
2014
(Array.isArray(value) ? value : value ? [value] : []).map((item) =>
@@ -43,7 +37,7 @@ const ClearedMultiSelectFilterable = ({
4337
disabled={isDisabled}
4438
{...rest}
4539
placeholder={carbonLabel || placeholder}
46-
onChange={originalOnChange}
40+
onChange={onChangeWrapper(onChange)}
4741
titleText={rest.labelText}
4842
id={rest.name}
4943
invalid={Boolean(invalidText)}
@@ -94,7 +88,7 @@ const ClearedMultiSelect = ({
9488
disabled={isDisabled}
9589
{...rest}
9690
label={carbonLabel || placeholder}
97-
onChange={originalOnChange}
91+
onChange={onChangeWrapper(onChange)}
9892
titleText={rest.labelText}
9993
id={rest.name}
10094
invalid={Boolean(invalidText)}
@@ -206,6 +200,7 @@ const ClearedSelectSearchable = ({
206200
originalOnChange,
207201
placeholder,
208202
labelText,
203+
onChange,
209204
...rest
210205
}) => (
211206
<ComboBox
@@ -218,7 +213,7 @@ const ClearedSelectSearchable = ({
218213
items={options}
219214
placeholder={placeholder}
220215
titleText={labelText}
221-
onChange={originalOnChange}
216+
onChange={onChangeWrapper(onChange)}
222217
/>
223218
);
224219

@@ -273,7 +268,6 @@ const Select = (props) => {
273268
loadOptions={loadOptions}
274269
invalidText={invalidText}
275270
loadOptionsChangeCounter={loadOptionsChangeCounter}
276-
originalOnChange={multiOnChange(input, rest.simpleValue)}
277271
helperText={text}
278272
/>
279273
);

packages/carbon-component-mapper/src/tests/select.test.js

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { FormRenderer, componentTypes } from '@data-driven-forms/react-form-rend
66
import FormTemplate from '../form-template';
77
import componentMapper from '../component-mapper';
88
import { Select, MultiSelect, ComboBox } from 'carbon-components-react';
9-
import { multiOnChange } from '../select';
109
import { getMultiValue } from '../select/select';
1110

1211
describe('<Select />', () => {
@@ -173,28 +172,6 @@ describe('<Select />', () => {
173172
});
174173
});
175174

176-
describe('multichange', () => {
177-
const input = {
178-
onChange: jest.fn()
179-
};
180-
181-
beforeEach(() => {
182-
input.onChange.mockReset();
183-
});
184-
185-
it('simpleValue', () => {
186-
multiOnChange(input, true)({ selectedItems: [{ value: '123' }, { value: '345' }] });
187-
188-
expect(input.onChange).toHaveBeenCalledWith(['123', '345']);
189-
});
190-
191-
it('not simple value', () => {
192-
multiOnChange(input, false)({ selectedItems: [{ value: '123' }, { value: '345' }] });
193-
194-
expect(input.onChange).toHaveBeenCalledWith([{ value: '123' }, { value: '345' }]);
195-
});
196-
});
197-
198175
describe('getMultiValue', () => {
199176
let value;
200177
let options;

packages/common/src/select/select.js

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,48 @@ import fnToString from '../utils/fn-to-string';
88
import reducer from './reducer';
99
import useIsMounted from '../hooks/use-is-mounted';
1010

11-
const getSelectValue = (stateValue, simpleValue, isMulti, allOptions) =>
12-
simpleValue ? allOptions.filter(({ value }) => (isMulti ? stateValue.includes(value) : isEqual(value, stateValue))) : stateValue;
11+
const getSelectValue = (stateValue, simpleValue, isMulti, allOptions) => {
12+
let enhancedValue = stateValue;
13+
14+
let hasSelectAll = isMulti && allOptions.find(({ selectAll }) => selectAll);
15+
let hasSelectNone = isMulti && allOptions.find(({ selectNone }) => selectNone);
16+
17+
if (hasSelectAll || hasSelectNone) {
18+
enhancedValue = enhancedValue || [];
19+
const optionsLength = allOptions.filter(({ selectAll, selectNone }) => !selectAll && !selectNone).length;
20+
21+
const selectedAll = optionsLength === enhancedValue.length;
22+
const selectedNone = enhancedValue.length === 0;
23+
24+
enhancedValue = [
25+
...enhancedValue,
26+
...(hasSelectAll && selectedAll ? [simpleValue ? hasSelectAll.value : hasSelectAll] : []),
27+
...(hasSelectNone && selectedNone ? [simpleValue ? hasSelectNone.value : hasSelectNone] : [])
28+
];
29+
}
30+
31+
return simpleValue ? allOptions.filter(({ value }) => (isMulti ? enhancedValue.includes(value) : isEqual(value, enhancedValue))) : enhancedValue;
32+
};
33+
34+
const handleSelectChange = (option, simpleValue, isMulti, onChange, allOptions, removeSelectAll, removeSelectNone) => {
35+
let enhanceOption = option;
36+
37+
if (removeSelectNone) {
38+
enhanceOption = enhanceOption.filter(({ selectNone }) => !selectNone);
39+
} else if (removeSelectAll) {
40+
enhanceOption = enhanceOption.filter(({ selectAll }) => !selectAll);
41+
}
42+
43+
const sanitizedOption = !enhanceOption && isMulti ? [] : enhanceOption;
44+
45+
if (isMulti && enhanceOption.find(({ selectAll }) => selectAll)) {
46+
return onChange(allOptions.filter(({ selectAll, selectNone }) => !selectAll && !selectNone).map(({ value }) => value));
47+
}
48+
49+
if (isMulti && enhanceOption.find(({ selectNone }) => selectNone)) {
50+
return onChange([]);
51+
}
1352

14-
const handleSelectChange = (option, simpleValue, isMulti, onChange) => {
15-
const sanitizedOption = !option && isMulti ? [] : option;
1653
return simpleValue
1754
? onChange(isMulti ? sanitizedOption.map((item) => item.value) : sanitizedOption ? sanitizedOption.value : undefined)
1855
: onChange(sanitizedOption);
@@ -136,6 +173,10 @@ const Select = ({
136173

137174
const selectValue = pluckSingleValue ? (isMulti ? value : Array.isArray(value) && value[0] ? value[0] : value) : value;
138175

176+
const filteredLength = state.options.filter(({ selectAll, selectNone }) => !selectAll && !selectNone).length;
177+
const shouldRemoveSelectAll = isMulti && state.options.find(({ selectAll }) => selectAll) && selectValue.length === filteredLength;
178+
const shouldRemoveSelectNone = isMulti && state.options.find(({ selectNone }) => selectNone) && selectValue.length === 0;
179+
139180
return (
140181
<SelectComponent
141182
className={clsx(classNamePrefix, {
@@ -147,7 +188,7 @@ const Select = ({
147188
classNamePrefix={classNamePrefix}
148189
isMulti={isMulti}
149190
value={getSelectValue(selectValue, simpleValue, isMulti, state.options)}
150-
onChange={(option) => handleSelectChange(option, simpleValue, isMulti, onChange)}
191+
onChange={(option) => handleSelectChange(option, simpleValue, isMulti, onChange, state.options, shouldRemoveSelectAll, shouldRemoveSelectNone)}
151192
onInputChange={onInputChange}
152193
isFetching={Object.values(state.promises).some((value) => value)}
153194
noOptionsMessage={renderNoOptionsMessage()}

packages/common/src/tests/select/select.test.js

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,174 @@ describe('Select test', () => {
313313
]);
314314
expect(inputValue).toEqual(['d', 'c']);
315315
});
316+
317+
it('selects all values', async () => {
318+
field = { ...field, isMulti: true, options: [{ label: 'Select all', selectAll: true }, ...field.options] };
319+
320+
await act(async () => {
321+
wrapper = mount(
322+
<FormRenderer
323+
{...rendererProps}
324+
schema={{
325+
fields: [
326+
{
327+
...field,
328+
component: componentTypes.SELECT,
329+
name: 'select'
330+
}
331+
]
332+
}}
333+
/>
334+
);
335+
});
336+
wrapper.update();
337+
338+
await act(async () => {
339+
wrapper
340+
.find('#onChange')
341+
.props()
342+
.onClick([{ label: 'Select all', selectAll: true }]);
343+
});
344+
wrapper.update();
345+
346+
expect(state.value).toEqual([
347+
{ label: 'Select all', selectAll: true },
348+
{ label: 'Dogs', value: 'd' },
349+
{ label: 'Cats', value: 'c' },
350+
{ label: 'Hamsters', value: 'h' }
351+
]);
352+
expect(inputValue).toEqual(['d', 'c', 'h']);
353+
354+
// remove hamsters
355+
await act(async () => {
356+
wrapper
357+
.find('#onChange')
358+
.props()
359+
.onClick([
360+
{ label: 'Select all', selectAll: true },
361+
{ label: 'Dogs', value: 'd' },
362+
{ label: 'Cats', value: 'c' }
363+
]);
364+
});
365+
wrapper.update();
366+
367+
expect(state.value).toEqual([
368+
{ label: 'Dogs', value: 'd' },
369+
{ label: 'Cats', value: 'c' }
370+
]);
371+
expect(inputValue).toEqual(['d', 'c']);
372+
});
373+
374+
it('selects none', async () => {
375+
field = { ...field, isMulti: true, options: [{ label: 'Select none', selectNone: true }, ...field.options], initialValue: ['d', 'c', 'h'] };
376+
377+
await act(async () => {
378+
wrapper = mount(
379+
<FormRenderer
380+
{...rendererProps}
381+
schema={{
382+
fields: [
383+
{
384+
...field,
385+
component: componentTypes.SELECT,
386+
name: 'select'
387+
}
388+
]
389+
}}
390+
/>
391+
);
392+
});
393+
wrapper.update();
394+
395+
await act(async () => {
396+
wrapper
397+
.find('#onChange')
398+
.props()
399+
.onClick([{ label: 'Select none', selectNone: true }]);
400+
});
401+
wrapper.update();
402+
403+
expect(state.value).toEqual([{ label: 'Select none', selectNone: true }]);
404+
expect(inputValue).toEqual('');
405+
406+
// adds one
407+
await act(async () => {
408+
wrapper
409+
.find('#onChange')
410+
.props()
411+
.onClick([
412+
{ label: 'Select none', selectNone: true },
413+
{ label: 'Dogs', value: 'd' }
414+
]);
415+
});
416+
wrapper.update();
417+
418+
expect(state.value).toEqual([{ label: 'Dogs', value: 'd' }]);
419+
expect(inputValue).toEqual(['d']);
420+
});
421+
422+
it('with select all and select none at that same time', async () => {
423+
field = {
424+
...field,
425+
isMulti: true,
426+
options: [
427+
{ label: 'Select all', selectAll: true, value: 'select-all' },
428+
{ label: 'Select none', selectNone: true, value: 'select-none' },
429+
...field.options
430+
]
431+
};
432+
433+
await act(async () => {
434+
wrapper = mount(
435+
<FormRenderer
436+
{...rendererProps}
437+
schema={{
438+
fields: [
439+
{
440+
...field,
441+
component: componentTypes.SELECT,
442+
name: 'select'
443+
}
444+
]
445+
}}
446+
/>
447+
);
448+
});
449+
wrapper.update();
450+
451+
await act(async () => {
452+
wrapper
453+
.find('#onChange')
454+
.props()
455+
.onClick([{ label: 'Select all', selectAll: true, value: 'select-all' }]);
456+
});
457+
wrapper.update();
458+
459+
expect(state.value).toEqual([
460+
{ label: 'Select all', selectAll: true, value: 'select-all' },
461+
{ label: 'Dogs', value: 'd' },
462+
{ label: 'Cats', value: 'c' },
463+
{ label: 'Hamsters', value: 'h' }
464+
]);
465+
expect(inputValue).toEqual(['d', 'c', 'h']);
466+
467+
await act(async () => {
468+
wrapper
469+
.find('#onChange')
470+
.props()
471+
.onClick([
472+
{ label: 'Select all', selectAll: true },
473+
{ label: 'Dogs', value: 'd' },
474+
{ label: 'Cats', value: 'c' },
475+
{ label: 'Hamsters', value: 'h' },
476+
{ label: 'Select none', selectNone: true, value: 'select-none' }
477+
]);
478+
});
479+
wrapper.update();
480+
481+
expect(state.value).toEqual([{ label: 'Select none', selectNone: true, value: 'select-none' }]);
482+
expect(inputValue).toEqual([]);
483+
});
316484
});
317485

318486
describe('loadOptions', () => {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import SelectCommon from '../select.md';
2+
3+
<SelectCommon/>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import SelectCommon from '../select.md';
2+
3+
<SelectCommon/>

packages/react-renderer-demo/src/doc-components/examples-texts/pf4/select.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,8 @@ const asyncLoadOptions = (searchValue) => new Promise(resolve => setTimeout(() =
2929

3030
return resolve(options);
3131
}, 2000));
32-
```
32+
```
33+
34+
import SelectCommon from '../select.md';
35+
36+
<SelectCommon/>

0 commit comments

Comments
 (0)