Skip to content

Commit 3a2ee81

Browse files
authored
Merge pull request #7410 from skateman/tree-view
Implemented the TreeViewSelector component for DDF
2 parents 1fe72a0 + a7e5c70 commit 3a2ee81

File tree

12 files changed

+862
-97
lines changed

12 files changed

+862
-97
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import React, { useState, useEffect } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { Tree, ActionTypes } from 'react-wooden-tree';
4+
5+
import { convert } from './helpers';
6+
7+
const TreeViewBase = ({
8+
actionMapper: overrideActionMapper,
9+
loadData,
10+
lazyLoadData,
11+
isMulti,
12+
check,
13+
select,
14+
...props
15+
}) => {
16+
const [nodes, setNodes] = useState([]);
17+
18+
const actionMapper = {
19+
[ActionTypes.CHECKED]: Tree.nodeChecked,
20+
[ActionTypes.CHECKED_DIRECTLY]: Tree.nodeChecked,
21+
[ActionTypes.DISABLED]: Tree.nodeDisabled,
22+
[ActionTypes.EXPANDED]: Tree.nodeExpanded,
23+
[ActionTypes.LOADING]: Tree.nodeLoading,
24+
[ActionTypes.SELECTED]: Tree.nodeSelected,
25+
[ActionTypes.CHILD_NODES]: (node, value) => {
26+
// There's a bug in the react-wooden-tree, the passed value here contains all the child
27+
// nodes and that would force the tree to mount nested children to the parent node twice.
28+
// For now we're filtering out the child nodes manually by counting the number of dots in
29+
// their identifier.
30+
const min = value.reduce((min, key) => {
31+
const nl = key.split('.').length;
32+
return nl > min ? min : nl;
33+
}, undefined);
34+
35+
return Tree.nodeChildren(node, value.filter(key => key.split('.').length === min));
36+
},
37+
...overrideActionMapper,
38+
};
39+
40+
useEffect(() => {
41+
loadData().then((values) => {
42+
setNodes(convert(values, { check, select }));
43+
});
44+
}, [loadData]);
45+
46+
const lazyLoad = node => lazyLoadData(node).then((result) => {
47+
const data = convert(result, { check, select });
48+
49+
return Object.keys(data).reduce((subtree, key) => {
50+
if (key !== '') {
51+
// Creating the node id from the parent id.
52+
const nodeId = `${node.nodeId}.${key}`;
53+
// Updating the children ids, so it does not point to something else.
54+
const element = { ...data[key], nodeId, nodes: data[key].nodes.map(child => `${node.nodeId}.${child}`) };
55+
return { ...subtree, [nodeId]: element };
56+
}
57+
58+
return subtree;
59+
}, {});
60+
});
61+
62+
const onDataChange = commands => setNodes(commands.reduce(
63+
(nodes, { type, value, nodeId }) => (
64+
type === ActionTypes.ADD_NODES
65+
? Tree.addNodes(nodes, value)
66+
: Tree.nodeUpdater(nodes, actionMapper[type](Tree.nodeSelector(nodes, nodeId), value))
67+
), nodes,
68+
));
69+
70+
return (
71+
<Tree
72+
nodeIcon=""
73+
expandIcon="fa fa-fw fa-angle-right"
74+
collapseIcon="fa fa-fw fa-angle-down"
75+
loadingIcon="fa fa-fw fa-spinner fa-pulse"
76+
checkedIcon="fa fa-fw fa-check-square-o"
77+
uncheckedIcon="fa fa-fw fa-square-o"
78+
selectedIcon=""
79+
partiallyCheckedIcon="fa fa-fw fa-check-square"
80+
preventDeselect
81+
data={nodes}
82+
showCheckbox={isMulti}
83+
callbacks={{ onDataChange, lazyLoad }}
84+
{...props}
85+
/>
86+
);
87+
};
88+
89+
TreeViewBase.propTypes = {
90+
actionMapper: PropTypes.any,
91+
loadData: PropTypes.func.isRequired,
92+
lazyLoadData: PropTypes.func,
93+
isMulti: PropTypes.bool,
94+
check: PropTypes.func,
95+
select: PropTypes.func,
96+
};
97+
98+
TreeViewBase.defaultProps = {
99+
actionMapper: {},
100+
lazyLoadData: () => undefined,
101+
isMulti: false,
102+
check: undefined,
103+
select: undefined,
104+
};
105+
106+
export default TreeViewBase;
Lines changed: 31 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,40 @@
1-
import React, { useState, useEffect } from 'react';
1+
import React from 'react';
22
import PropTypes from 'prop-types';
3-
import {Tree, ActionTypes } from 'react-wooden-tree';
3+
import { Tree, ActionTypes } from 'react-wooden-tree';
44
import {
55
ControlLabel, FieldLevelHelp, FormGroup, HelpBlock,
66
} from 'patternfly-react';
7+
import { useFieldApi, useFormApi } from '@data-driven-forms/react-form-renderer';
78

89
import RequiredLabel from '../../forms/required-label';
9-
import { convert, flatten } from './helpers';
10-
import { useFieldApi, useFormApi } from '@@ddf';
10+
import TreeViewBase from './base';
1111

12-
const setChecked = (keys = []) => ({ state, ...node }) => ({ ...node, state: { ...state, checked: keys.includes(node.attr.key) } });
13-
14-
const TreeViewField = ({ loadData, lazyLoadData, ...props }) => {
15-
const [{ nodes }, setState] = useState({});
16-
17-
const { input: { value }, meta } = useFieldApi(props);
12+
const TreeViewField = ({
13+
loadData, lazyLoadData, helperText, isRequired, label, identifier, ...props
14+
}) => {
15+
const { input: { value = [], name }, meta } = useFieldApi(props);
1816
const formOptions = useFormApi();
1917

20-
useEffect(() => {
21-
loadData().then((values) => {
22-
setState(state => ({ ...state, nodes: flatten(convert(values, setChecked(value))) }));
23-
});
24-
}, [loadData]);
25-
2618
const actionMapper = {
27-
[ActionTypes.EXPANDED]: Tree.nodeExpanded,
28-
[ActionTypes.CHECKED]: Tree.nodeChecked,
29-
[ActionTypes.DISABLED]: Tree.nodeDisabled,
30-
[ActionTypes.SELECTED]: Tree.nodeSelected,
31-
[ActionTypes.LOADING]: Tree.nodeLoading,
32-
[ActionTypes.CHILD_NODES]: (node, value) => {
33-
// There's a bug in the react-wooden-tree, the passed value here contains all the child
34-
// nodes and that would force the tree to mount nested children to the parent node twice.
35-
// For now we're filtering out the child nodes manually by counting the number of dots in
36-
// their identifier.
37-
const min = value.reduce((min, key) => {
38-
const nl = key.split('.').length;
39-
return nl > min ? min : nl;
40-
}, undefined);
41-
42-
return Tree.nodeChildren(node, value.filter(key => key.split('.').length === min));
43-
},
4419
[ActionTypes.CHECKED_DIRECTLY]: (node, value) => {
45-
const values = formOptions.getFieldState(props.name).value;
46-
formOptions.change(props.name, value ? [...values, node.attr.key] : values.filter(item => item !== node.attr.key));
47-
20+
const { value: values = [] } = formOptions.getFieldState(name);
21+
formOptions.change(name, value ? [...values, identifier(node)] : values.filter(item => item !== identifier(node)));
4822
return Tree.nodeChecked(node, value);
4923
},
5024
};
5125

52-
const lazyLoad = node => lazyLoadData(node).then((result) => {
53-
const data = flatten(convert(result, setChecked(value)));
54-
55-
let subtree = {};
56-
Object.keys(data).forEach((key) => {
57-
if (key !== '') {
58-
// Creating the node id from the parent id.
59-
const nodeId = `${node.nodeId}.${key}`;
60-
// Updating the children ids, so it does not point to something else.
61-
const element = { ...data[key], nodeId, nodes: data[key].nodes.map(child => `${node.nodeId}.${child}`) };
62-
subtree = { ...subtree, [nodeId]: element };
63-
}
64-
});
65-
return subtree;
66-
});
67-
68-
const onDataChange = commands => setState(state => ({
69-
...state,
70-
nodes: commands.reduce(
71-
(nodes, { type, value, nodeId }) => (
72-
type === ActionTypes.ADD_NODES
73-
? Tree.addNodes(nodes, value)
74-
: Tree.nodeUpdater(nodes, actionMapper[type](Tree.nodeSelector(nodes, nodeId), value))
75-
), nodes,
76-
),
77-
}));
78-
7926
return (
8027
<FormGroup validationState={meta.error ? 'error' : null}>
8128
<ControlLabel>
82-
{props.isRequired ? <RequiredLabel label={props.label} /> : props.label }
83-
{props.helperText && <FieldLevelHelp content={props.helperText} />}
29+
{isRequired ? <RequiredLabel label={label} /> : label }
30+
{helperText && <FieldLevelHelp content={helperText} />}
8431
</ControlLabel>
85-
<Tree
86-
data={nodes}
87-
nodeIcon=""
88-
expandIcon="fa fa-fw fa-angle-right"
89-
collapseIcon="fa fa-fw fa-angle-down"
90-
loadingIcon="fa fa-fw fa-spinner fa-pulse"
91-
checkedIcon="fa fa-fw fa-check-square-o"
92-
uncheckedIcon="fa fa-fw fa-square-o"
93-
selectedIcon=""
94-
partiallyCheckedIcon="fa fa-fw fa-check-square"
95-
preventDeselect
96-
showCheckbox
97-
callbacks={{ onDataChange, lazyLoad }}
32+
<TreeViewBase
33+
loadData={loadData}
34+
lazyLoadData={lazyLoadData}
35+
actionMapper={actionMapper}
36+
check={node => value.includes(identifier(node))}
37+
isMulti
9838
{...props}
9939
/>
10040
{meta.error && <HelpBlock>{ meta.error }</HelpBlock>}
@@ -103,8 +43,20 @@ const TreeViewField = ({ loadData, lazyLoadData, ...props }) => {
10343
};
10444

10545
TreeViewField.propTypes = {
106-
loadData: PropTypes.func,
46+
loadData: PropTypes.func.isRequired,
10747
lazyLoadData: PropTypes.func,
48+
helperText: PropTypes.string,
49+
isRequired: PropTypes.bool,
50+
label: PropTypes.string,
51+
identifier: PropTypes.func,
52+
};
53+
54+
TreeViewField.defaultProps = {
55+
lazyLoadData: () => undefined,
56+
helperText: undefined,
57+
isRequired: false,
58+
label: undefined,
59+
identifier: node => node.attr.key,
10860
};
10961

11062
export default TreeViewField;

app/javascript/components/tree-view/helpers.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Tree } from 'react-wooden-tree';
66
* Sets undefined checkbox to null.
77
* Moves the key to attr.key.
88
*/
9-
const convert = (tree, fn) => tree.map((n) => {
9+
const normalize = (tree, fn) => tree.map((n) => {
1010
let node = { ...n };
1111
if (node.class) {
1212
node = { ...node, classes: node.class };
@@ -26,14 +26,27 @@ const convert = (tree, fn) => tree.map((n) => {
2626
}
2727

2828
if (node.nodes) {
29-
node = { ...node, nodes: convert(node.nodes, fn) };
29+
node = { ...node, nodes: normalize(node.nodes, fn) };
3030
}
3131

3232
return fn ? fn(node) : node;
3333
});
3434

3535
const flatten = data => Tree.convertHierarchicalTree(Tree.initHierarchicalTree(data));
3636

37+
// This function adjusts the nodes generated by the TreeBuilder for the usage with TreeView
38+
const convert = (data, { check, select } = {}) => flatten(normalize(
39+
data,
40+
({ state: { checked, selected, ...state } = {}, ...node }) => ({
41+
...node,
42+
state: {
43+
...state,
44+
checked: check ? check(node) : checked,
45+
selected: select ? select(node) : selected,
46+
},
47+
}),
48+
));
49+
3750
/**
3851
* Helper
3952
* Activates the first active node in the tree, which is passed trought the props.
@@ -54,4 +67,4 @@ const activateNode = (tree, doActivate, key) => {
5467

5568
const callBack = (nodeId, type, value, namespace) => ({ nodeId, type, value, namespace });
5669

57-
export { convert, flatten, activateNode, callBack };
70+
export { convert, activateNode, callBack };
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
export { default as HierarchicalTreeView } from './hierarchical-tree-view';
1+
import TreeViewRedux from './redux';
2+
import TreeViewField from './field';
3+
import TreeViewSelector from './selector';
4+
5+
export { TreeViewRedux, TreeViewField, TreeViewSelector };

app/javascript/components/tree-view/hierarchical-tree-view.jsx renamed to app/javascript/components/tree-view/redux.jsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import { combineReducers } from '../../helpers/redux';
99
import reducers, { ACTIONS } from './reducers/index';
1010
import basicStore from './reducers/basicStore';
1111
import {
12-
convert, callBack, activateNode, flatten,
12+
convert, callBack, activateNode,
1313
} from './helpers';
1414

15-
const HierarchicalTreeView = (props) => {
15+
const TreeView = (props) => {
1616
const {
1717
tree_name,
1818
bs_tree,
@@ -59,7 +59,7 @@ const HierarchicalTreeView = (props) => {
5959
*/
6060
useEffect(() => {
6161
// FIXME - When the conversion wont be needed hopefuly in the future
62-
const tree = activateNode(flatten(convert(JSON.parse(bs_tree))), silent_activate, select_node);
62+
const tree = activateNode(convert(JSON.parse(bs_tree), node => node.state.checked, node => node.state.selected), silent_activate, select_node);
6363

6464
callBack(null, ACTIONS.EMPTY_TREE, null, namespace);
6565
callBack(null, ACTIONS.ADD_NODES, tree, namespace);
@@ -81,7 +81,7 @@ const HierarchicalTreeView = (props) => {
8181
tree: tree_name,
8282
mode: 'all',
8383
}).then((result) => {
84-
const data = flatten(convert(result));
84+
const data = convert(result);
8585
let subtree = {};
8686
Object.keys(data).forEach((key) => {
8787
if (key !== '') {
@@ -117,7 +117,7 @@ const HierarchicalTreeView = (props) => {
117117
);
118118
};
119119

120-
HierarchicalTreeView.propTypes = {
120+
TreeView.propTypes = {
121121
tree_name: PropTypes.string.isRequired,
122122
bs_tree: PropTypes.string.isRequired,
123123
checkboxes: PropTypes.bool,
@@ -130,7 +130,7 @@ HierarchicalTreeView.propTypes = {
130130
hierarchical_check: PropTypes.bool,
131131
};
132132

133-
HierarchicalTreeView.defaultProps = {
133+
TreeView.defaultProps = {
134134
checkboxes: false,
135135
allow_reselect: false,
136136
oncheck: undefined,
@@ -140,6 +140,6 @@ HierarchicalTreeView.defaultProps = {
140140
hierarchical_check: false,
141141
};
142142

143-
const HierarchicalTreeViewConn = connect(null, { callBack })(HierarchicalTreeView);
143+
const TreeViewRedux = connect(null, { callBack })(TreeView);
144144

145-
export default HierarchicalTreeViewConn;
145+
export default TreeViewRedux;

0 commit comments

Comments
 (0)