Skip to content

Commit 6bd0e4c

Browse files
authored
Merge pull request #1148 from rvsia/pf4SelectCategories
Add groups/dividers to PF4 select
2 parents 3151ceb + 9f4ce7b commit 6bd0e4c

File tree

14 files changed

+400
-55
lines changed

14 files changed

+400
-55
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { ReactNode } from "react";
2+
3+
interface Option {
4+
label: string | ReactNode;
5+
value: any;
6+
selectAll?: boolean;
7+
selectNone?: boolean;
8+
}
9+
10+
interface ResultedOption {
11+
label?: string | ReactNode;
12+
value?: any;
13+
selectAll?: boolean;
14+
selectNone?: boolean;
15+
group?: string | ReactNode;
16+
divider?: boolean;
17+
}
18+
19+
20+
interface Options {
21+
label?: string | ReactNode;
22+
value?: any;
23+
divider?: boolean;
24+
selectAll?: boolean;
25+
selectNone?: boolean;
26+
options?: Option[];
27+
}
28+
29+
declare const flatOptions: (options: Options[]) => ResultedOption[];
30+
31+
export default flatOptions;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const flatOptions = (options) => options.flatMap((option) => (option.options ? [{ group: option.label }, ...option.options] : [option]));
2+
3+
export default flatOptions;

packages/common/src/select/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { default } from './select';
22
export * from './select';
33
export { default as parseInternalValue } from './parse-internal-value';
4+
export { default as flatOptions } from './flat-options';

packages/common/src/select/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { default } from './select';
22
export { default as parseInternalValue } from './parse-internal-value';
3+
export { default as flatOptions } from './flat-options';

packages/common/src/select/reducer.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1-
const reducer = (state, { type, payload, options = [] }) => {
1+
export const init = ({ propsOptions, optionsTransformer }) => ({
2+
isLoading: false,
3+
options: optionsTransformer ? optionsTransformer(propsOptions) : propsOptions,
4+
promises: {},
5+
isInitialLoaded: false,
6+
...(optionsTransformer && { originalOptions: propsOptions }),
7+
});
8+
9+
const reducer = (state, { type, payload, options = [], optionsTransformer }) => {
210
switch (type) {
311
case 'updateOptions':
412
return {
513
...state,
6-
options: payload,
14+
options: optionsTransformer ? optionsTransformer(payload) : payload,
715
isLoading: false,
816
promises: {},
17+
...(optionsTransformer && { originalOptions: payload }),
918
};
1019
case 'startLoading':
1120
return {
@@ -15,7 +24,8 @@ const reducer = (state, { type, payload, options = [] }) => {
1524
case 'setOptions':
1625
return {
1726
...state,
18-
options: payload,
27+
options: optionsTransformer ? optionsTransformer(payload) : payload,
28+
...(optionsTransformer && { originalOptions: payload }),
1929
};
2030
case 'initialLoaded':
2131
return {
@@ -29,7 +39,12 @@ const reducer = (state, { type, payload, options = [] }) => {
2939
...state.promises,
3040
...payload,
3141
},
32-
options: [...state.options, ...options.filter(({ value }) => !state.options.find((option) => option.value === value))],
42+
options: optionsTransformer
43+
? optionsTransformer([...state.options, ...options.filter(({ value }) => !state.options.find((option) => option.value === value))])
44+
: [...state.options, ...options.filter(({ value }) => !state.options.find((option) => option.value === value))],
45+
...(optionsTransformer && {
46+
originalOptions: [...state.options, ...options.filter(({ value }) => !state.options.find((option) => option.value === value))],
47+
}),
3348
};
3449
default:
3550
return state;

packages/common/src/select/select.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface SelectProps {
2828
isSearchable?: boolean;
2929
SelectComponent?: React.ComponentType;
3030
noValueUpdates?: boolean;
31+
optionsTransformer?: (options: AnyObject[]) => option[];
3132
}
3233

3334
declare const Select: React.ComponentType<SelectProps>;

packages/common/src/select/select.js

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
55
import clsx from 'clsx';
66
import isEqual from 'lodash/isEqual';
77
import fnToString from '../utils/fn-to-string';
8-
import reducer from './reducer';
8+
import reducer, { init } from './reducer';
99
import useIsMounted from '../hooks/use-is-mounted';
1010

1111
const getSelectValue = (stateValue, simpleValue, isMulti, allOptions) => {
@@ -16,7 +16,9 @@ const getSelectValue = (stateValue, simpleValue, isMulti, allOptions) => {
1616

1717
if (hasSelectAll || hasSelectNone) {
1818
enhancedValue = enhancedValue || [];
19-
const optionsLength = allOptions.filter(({ selectAll, selectNone }) => !selectAll && !selectNone).length;
19+
const optionsLength = allOptions.filter(
20+
({ selectAll, selectNone, divider, options }) => !selectAll && !selectNone && !divider && !options
21+
).length;
2022

2123
const selectedAll = optionsLength === enhancedValue.length;
2224
const selectedNone = enhancedValue.length === 0;
@@ -43,7 +45,7 @@ const handleSelectChange = (option, simpleValue, isMulti, onChange, allOptions,
4345
const sanitizedOption = !enhanceOption && isMulti ? [] : enhanceOption;
4446

4547
if (isMulti && sanitizedOption.find(({ selectAll }) => selectAll)) {
46-
return onChange(allOptions.filter(({ selectAll, selectNone }) => !selectAll && !selectNone).map(({ value }) => value));
48+
return onChange(allOptions.filter(({ selectAll, selectNone, value }) => !selectAll && !selectNone && value).map(({ value }) => value));
4749
}
4850

4951
if (isMulti && sanitizedOption.find(({ selectNone }) => selectNone)) {
@@ -73,14 +75,11 @@ const Select = ({
7375
loadOptionsChangeCounter,
7476
SelectComponent,
7577
noValueUpdates,
78+
optionsTransformer,
7679
...props
7780
}) => {
78-
const [state, dispatch] = useReducer(reducer, {
79-
isLoading: false,
80-
options: propsOptions,
81-
promises: {},
82-
isInitialLoaded: false,
83-
});
81+
const [state, originalDispatch] = useReducer(reducer, { optionsTransformer, propsOptions }, init);
82+
const dispatch = (action) => originalDispatch({ ...action, optionsTransformer });
8483

8584
const isMounted = useIsMounted();
8685

@@ -145,6 +144,7 @@ const Select = ({
145144
onChange={() => {}}
146145
{...loadingProps}
147146
noOptionsMessage={renderNoOptionsMessage()}
147+
{...(state.originalOptions && { originalOptions: state.originalOptions })}
148148
/>
149149
);
150150
}
@@ -194,6 +194,7 @@ const Select = ({
194194
noOptionsMessage={renderNoOptionsMessage()}
195195
hideSelectedOptions={false}
196196
closeMenuOnSelect={!isMulti}
197+
{...(state.originalOptions && { originalOptions: state.originalOptions })}
197198
/>
198199
);
199200
};
@@ -220,6 +221,7 @@ Select.propTypes = {
220221
isSearchable: PropTypes.bool,
221222
SelectComponent: PropTypes.elementType.isRequired,
222223
noValueUpdates: PropTypes.bool,
224+
optionsTransformer: PropTypes.func,
223225
};
224226

225227
Select.defaultProps = {

packages/pf4-component-mapper/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@
3030
"javascript"
3131
],
3232
"devDependencies": {
33-
"@patternfly/react-core": "^4.101.3",
34-
"@patternfly/react-icons": "^4.9.5"
33+
"@patternfly/react-core": "^4.157.3",
34+
"@patternfly/react-icons": "^4.11.7"
3535
},
3636
"peerDependencies": {
3737
"@data-driven-forms/react-form-renderer": ">=3.2.1",
38-
"@patternfly/react-core": "^4.101.3",
39-
"@patternfly/react-icons": "^4.9.5"
38+
"@patternfly/react-core": "^4.157.3",
39+
"@patternfly/react-icons": "^4.11.7"
4040
},
4141
"dependencies": {
4242
"@data-driven-forms/common": "*",

packages/pf4-component-mapper/src/select/select/menu.js

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,26 @@ const Menu = ({
117117
menuPortalTarget,
118118
menuIsPortal,
119119
selectToggleRef,
120+
originalOptions,
120121
}) => {
121-
const filteredOptions = isSearchable ? filterOptions(options, filterValue) : options;
122+
const filteredOptions = isSearchable ? filterOptions(originalOptions, filterValue) : originalOptions;
123+
124+
let index = 0;
125+
126+
const createOption = (item) => {
127+
index++;
128+
129+
const itemProps = getItemProps({
130+
item,
131+
index,
132+
isActive: highlightedIndex === index,
133+
isSelected: isMulti ? !!selectedItem.find(({ value }) => item.value === value) : selectedItem === item.value,
134+
onMouseUp: (e) => e.stopPropagation(), // we need this to prevent issues with portal menu not selecting a option
135+
});
136+
137+
return <Option key={item.key || item.value || (typeof item.label === 'string' && item.label) || item} item={item} {...itemProps} />;
138+
};
139+
122140
const menuItems = (
123141
<ul className={`pf-c-select__menu${menuIsPortal ? ' ddorg__pf4-component-mapper__select-menu-portal' : ''}`}>
124142
{filteredOptions.length === 0 && (
@@ -130,15 +148,21 @@ const Menu = ({
130148
isFetching={isFetching}
131149
/>
132150
)}
133-
{filteredOptions.map((item, index) => {
134-
const itemProps = getItemProps({
135-
item,
136-
index,
137-
isActive: highlightedIndex === index,
138-
isSelected: isMulti ? !!selectedItem.find(({ value }) => item.value === value) : selectedItem === item.value,
139-
onMouseUp: (e) => e.stopPropagation(), // we need this to prevent issues with portal menu not selecting a option
140-
});
141-
return <Option key={item.key || item.value || (typeof item.label === 'string' && item.label) || item} item={item} {...itemProps} />;
151+
{filteredOptions.map((item, arrayIndex) => {
152+
if (item.options) {
153+
return (
154+
<div className="pf-c-select__menu-group" key={`group-${arrayIndex}`}>
155+
<div className="pf-c-select__menu-group-title">{item.label}</div>
156+
{item.options.map((nestedItem) => createOption(nestedItem))}
157+
</div>
158+
);
159+
}
160+
161+
if (item.divider) {
162+
return <hr className="pf-c-divider" key={`divider-${index}`} />;
163+
}
164+
165+
return createOption(item);
142166
})}
143167
</ul>
144168
);

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

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useRef, useState } from 'react';
22
import PropTypes from 'prop-types';
33

4-
import DataDrivenSelect from '@data-driven-forms/common/select';
4+
import DataDrivenSelect, { flatOptions } from '@data-driven-forms/common/select';
55
import parseInternalValue from '@data-driven-forms/common/select/parse-internal-value';
66
import Downshift from 'downshift';
77
import { CaretDownIcon, CloseIcon, CircleNotchIcon } from '@patternfly/react-icons';
@@ -69,11 +69,36 @@ const itemToString = (value, isMulti, showMore, handleShowMore, handleChange) =>
6969
};
7070

7171
// TODO fix the value of internal select not to be an array all the time. It forces the filter value to be an array and it crashes sometimes.
72-
const filterOptions = (options, filterValue = '') =>
73-
options.filter(({ label }) => {
74-
const filter = Array.isArray(filterValue) && filterValue.length > 0 ? filterValue[0] : filterValue;
75-
return label.toLowerCase().includes(filter.toLowerCase());
76-
});
72+
const filterOptions = (options, filterValue = '') => {
73+
const filter = (Array.isArray(filterValue) && filterValue.length > 0 ? filterValue[0] : filterValue).toLowerCase();
74+
75+
if (!filter) {
76+
return options;
77+
}
78+
79+
return options
80+
.map((option) => {
81+
if (option.options) {
82+
const filteredNested = option.options.map((option) => (option.label?.toLowerCase().includes(filter) ? option : null)).filter(Boolean);
83+
84+
if (filteredNested.length === 0) {
85+
return null;
86+
}
87+
88+
return {
89+
...option,
90+
options: filteredNested,
91+
};
92+
}
93+
94+
if (option.label?.toLowerCase().includes(filter)) {
95+
return option;
96+
}
97+
98+
return null;
99+
})
100+
.filter(Boolean);
101+
};
77102

78103
const getValue = (isMulti, option, value) => {
79104
if (!isMulti || !option) {
@@ -154,6 +179,7 @@ const InternalSelect = ({
154179
loadingMessage,
155180
menuPortalTarget,
156181
menuIsPortal,
182+
originalOptions,
157183
...props
158184
}) => {
159185
const [showMore, setShowMore] = useState(false);
@@ -223,6 +249,7 @@ const InternalSelect = ({
223249
menuPortalTarget={menuPortalTarget}
224250
menuIsPortal={menuIsPortal}
225251
selectToggleRef={selectToggleRef}
252+
originalOptions={originalOptions}
226253
/>
227254
)}
228255
</div>
@@ -238,6 +265,7 @@ InternalSelect.propTypes = {
238265
PropTypes.shape({
239266
value: PropTypes.any,
240267
label: PropTypes.any,
268+
divider: PropTypes.bool,
241269
})
242270
).isRequired,
243271
value: PropTypes.any,
@@ -256,12 +284,21 @@ InternalSelect.propTypes = {
256284
loadingMessage: PropTypes.node,
257285
menuPortalTarget: PropTypes.any,
258286
menuIsPortal: PropTypes.bool,
287+
originalOptions: PropTypes.array,
259288
};
260289

261290
const Select = ({ menuIsPortal, ...props }) => {
262291
const menuPortalTarget = menuIsPortal ? document.body : undefined;
263292

264-
return <DataDrivenSelect SelectComponent={InternalSelect} menuPortalTarget={menuPortalTarget} menuIsPortal={menuIsPortal} {...props} />;
293+
return (
294+
<DataDrivenSelect
295+
SelectComponent={InternalSelect}
296+
menuPortalTarget={menuPortalTarget}
297+
menuIsPortal={menuIsPortal}
298+
{...props}
299+
optionsTransformer={flatOptions}
300+
/>
301+
);
265302
};
266303

267304
Select.propTypes = {

0 commit comments

Comments
 (0)