Skip to content

Commit 3bb1032

Browse files
authored
Merge pull request #1018 from rvsia/introduceDualListTreeSelect
feat(pf4): introduce dual list tree select component
2 parents fa041d2 + e4e9d64 commit 3bb1032

File tree

9 files changed

+592
-2
lines changed

9 files changed

+592
-2
lines changed

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { DualListSelector } from '@patternfly/react-core';
44
import { useFieldApi } from '@data-driven-forms/react-form-renderer';
55
import isEqual from 'lodash/isEqual';
66

7+
import DualListTree from '../dual-list-tree-select/dual-list-tree-select';
8+
79
import FormGroup from '../form-group';
810
import DualListContext from '../dual-list-context';
911

@@ -124,4 +126,10 @@ DualList.propTypes = {
124126
isSortable: PropTypes.bool
125127
};
126128

127-
export default DualList;
129+
const DualListWrapper = (props) => props.isTree ? <DualListTree {...props} /> : <DualList {...props}/>;
130+
131+
DualListWrapper.propTypes = {
132+
isTree: PropTypes.bool,
133+
};
134+
135+
export default DualListWrapper;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { UseFieldApiComponentConfig } from "@data-driven-forms/react-form-renderer";
2+
import { DualListSelectorProps } from "@patternfly/react-core";
3+
import { DualListSelectorTreeItemProps } from "@patternfly/react-core/dist/js/components/DualListSelector/DualListSelectorTreeItem";
4+
import FormGroupProps from "../form-group";
5+
6+
export interface DualListTreeSelectOption extends DualListSelectorTreeItemProps {
7+
value: any;
8+
}
9+
10+
interface InternalDualListSelectProps {
11+
options: Array<DualListTreeSelectOption | string>;
12+
isSortable?: boolean;
13+
}
14+
15+
export type DualListTreeSelectProps = InternalDualListSelectProps & FormGroupProps & UseFieldApiComponentConfig & DualListSelectorProps;
16+
17+
declare const DualListSelectTree: React.ComponentType<DualListTreeSelectProps>;
18+
19+
export default DualListSelectTree;
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import React, { useState } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { DualListSelector } from '@patternfly/react-core';
4+
import { useFieldApi } from '@data-driven-forms/react-form-renderer';
5+
import isEqual from 'lodash/isEqual';
6+
7+
import FormGroup from '../form-group';
8+
import DualListContext from '../dual-list-context';
9+
10+
export const convertOptions = (options, sort) => {
11+
if (Array.isArray(options)) {
12+
let result = options.map((option) => convertOptions(option, sort)).filter(Boolean);
13+
14+
if (sort) {
15+
const sortFn = (a, b) => (sort === 'asc' ? a.localeCompare(b) : b.localeCompare(a));
16+
17+
result = result.sort((a, b) => sortFn(a.text || a.label, b.text || b.label));
18+
}
19+
20+
return result;
21+
}
22+
23+
return {
24+
text: options.label,
25+
id: options.value || options.key || options.label || options.text,
26+
isChecked: false,
27+
...options,
28+
...(options.children && { children: convertOptions(options.children, sort).filter(Boolean) })
29+
};
30+
};
31+
32+
export const selectedOptions = (options, value, selected) => {
33+
if (Array.isArray(options)) {
34+
return options.map((option) => selectedOptions(option, value, selected)).filter(Boolean);
35+
}
36+
37+
if (options.value) {
38+
if (selected ? value.includes(options.value) : !value.includes(options.value)) {
39+
return options;
40+
}
41+
}
42+
43+
if (options.children) {
44+
const someSelected = selectedOptions(options.children, value, selected).filter(Boolean);
45+
46+
if (someSelected.length) {
47+
return {
48+
...options,
49+
children: someSelected
50+
};
51+
}
52+
}
53+
};
54+
55+
export const getValueFromSelected = (options, newValue = []) => {
56+
if (Array.isArray(options)) {
57+
options.map((option) => getValueFromSelected(option, newValue));
58+
}
59+
60+
if (options.value) {
61+
newValue.push(options.value);
62+
}
63+
64+
if (options.children) {
65+
getValueFromSelected(options.children, newValue);
66+
}
67+
68+
return newValue;
69+
};
70+
71+
const DualListTreeSelect = (props) => {
72+
const {
73+
label,
74+
isRequired,
75+
helperText,
76+
meta,
77+
validateOnMount,
78+
description,
79+
hideLabel,
80+
id,
81+
input,
82+
FormGroupProps,
83+
options,
84+
isSortable,
85+
...rest
86+
} = useFieldApi({
87+
...props,
88+
FieldProps: {
89+
isEqual: (current, initial) => isEqual([...(current || [])].sort(), [...(initial || [])].sort())
90+
}
91+
});
92+
93+
const [sortConfig, setSortConfig] = useState(() => ({ left: isSortable && 'asc', right: isSortable && 'asc' }));
94+
95+
const value = input.value || [];
96+
97+
const leftOptions = selectedOptions(options, value, false);
98+
const rightOptions = selectedOptions(options, value, true);
99+
100+
const onListChange = (_newLeft, newRight) => input.onChange(getValueFromSelected(newRight));
101+
102+
return (
103+
<FormGroup
104+
label={label}
105+
isRequired={isRequired}
106+
helperText={helperText}
107+
meta={meta}
108+
validateOnMount={validateOnMount}
109+
description={description}
110+
hideLabel={hideLabel}
111+
id={id || input.name}
112+
FormGroupProps={FormGroupProps}
113+
>
114+
<DualListContext.Provider value={{ sortConfig, setSortConfig }}>
115+
<DualListSelector
116+
availableOptions={convertOptions(leftOptions, sortConfig.left)}
117+
chosenOptions={convertOptions(rightOptions, sortConfig.right)}
118+
onListChange={onListChange}
119+
id={id || input.name}
120+
isTree
121+
{...rest}
122+
/>
123+
</DualListContext.Provider>
124+
</FormGroup>
125+
);
126+
};
127+
128+
DualListTreeSelect.propTypes = {
129+
label: PropTypes.node,
130+
validateOnMount: PropTypes.bool,
131+
isRequired: PropTypes.bool,
132+
helperText: PropTypes.node,
133+
description: PropTypes.node,
134+
hideLabel: PropTypes.bool,
135+
id: PropTypes.string,
136+
isSearchable: PropTypes.bool,
137+
isSortable: PropTypes.bool
138+
};
139+
140+
export default DualListTreeSelect;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './dual-list-tree-select';
2+
export * from './dual-list-tree-select';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './dual-list-tree-select';
2+
export * from './dual-list-tree-select';

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export { default as FormTemplate } from './form-template';
33
export { default as Checkbox } from './checkbox';
44
export { default as DatePicker } from './date-picker';
55
export { default as DualListSelect } from './dual-list-select';
6+
export { default as DualListSelectTree } from './dual-list-tree-select';
67
export { default as DualListContext } from './dual-list-context';
78
export { default as DualListSortButton } from './dual-list-sort-button';
89
export { default as FieldArray } from './field-array';

packages/pf4-component-mapper/src/tests/dual-list-select.test.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,35 @@ describe('DualListSelect', () => {
8888
<span key="pigeons">pigeons</span>
8989
]
9090
}
91-
]
91+
],
92+
[
93+
'tree variant',
94+
{
95+
isTree: true,
96+
options: [
97+
{
98+
value: 'cats',
99+
label: 'cats'
100+
},
101+
{
102+
value: 'cats_1',
103+
label: 'cats_1'
104+
},
105+
{
106+
value: 'cats_2',
107+
label: 'cats_2'
108+
},
109+
{
110+
value: 'zebras',
111+
label: 'zebras'
112+
},
113+
{
114+
value: 'pigeons',
115+
label: 'pigeons'
116+
}
117+
]
118+
}
119+
],
92120
].forEach(([title, props]) => {
93121
describe(`${title} values`, () => {
94122
beforeEach(() => {
@@ -290,10 +318,12 @@ describe('DualListSelect', () => {
290318
).toHaveLength(schema.fields[0].options.length);
291319
await act(async () => {
292320
wrapper
321+
.find('.pf-c-dual-list-selector__tools-filter')
293322
.find('input')
294323
.last()
295324
.instance().value = 'cats';
296325
wrapper
326+
.find('.pf-c-dual-list-selector__tools-filter')
297327
.find('input')
298328
.last()
299329
.simulate('change');

0 commit comments

Comments
 (0)