Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions app/javascript/components/tree-view/base.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Tree, ActionTypes } from 'react-wooden-tree';

import { convert } from './helpers';

const TreeViewBase = ({
actionMapper: overrideActionMapper,
loadData,
lazyLoadData,
isMulti,
check,
select,
...props
}) => {
const [nodes, setNodes] = useState([]);

const actionMapper = {
[ActionTypes.CHECKED]: Tree.nodeChecked,
[ActionTypes.CHECKED_DIRECTLY]: Tree.nodeChecked,
[ActionTypes.DISABLED]: Tree.nodeDisabled,
[ActionTypes.EXPANDED]: Tree.nodeExpanded,
[ActionTypes.LOADING]: Tree.nodeLoading,
[ActionTypes.SELECTED]: Tree.nodeSelected,
[ActionTypes.CHILD_NODES]: (node, value) => {
// There's a bug in the react-wooden-tree, the passed value here contains all the child
// nodes and that would force the tree to mount nested children to the parent node twice.
// For now we're filtering out the child nodes manually by counting the number of dots in
// their identifier.
const min = value.reduce((min, key) => {
const nl = key.split('.').length;
return nl > min ? min : nl;
}, undefined);

return Tree.nodeChildren(node, value.filter(key => key.split('.').length === min));
},
...overrideActionMapper,
};

useEffect(() => {
loadData().then((values) => {
setNodes(convert(values, { check, select }));
});
}, [loadData]);

const lazyLoad = node => lazyLoadData(node).then((result) => {
const data = convert(result, { check, select });

return Object.keys(data).reduce((subtree, key) => {
if (key !== '') {
// Creating the node id from the parent id.
const nodeId = `${node.nodeId}.${key}`;
// Updating the children ids, so it does not point to something else.
const element = { ...data[key], nodeId, nodes: data[key].nodes.map(child => `${node.nodeId}.${child}`) };
return { ...subtree, [nodeId]: element };
}

return subtree;
}, {});
});

const onDataChange = commands => setNodes(commands.reduce(
(nodes, { type, value, nodeId }) => (
type === ActionTypes.ADD_NODES
? Tree.addNodes(nodes, value)
: Tree.nodeUpdater(nodes, actionMapper[type](Tree.nodeSelector(nodes, nodeId), value))
), nodes,
));

return (
<Tree
nodeIcon=""
expandIcon="fa fa-fw fa-angle-right"
collapseIcon="fa fa-fw fa-angle-down"
loadingIcon="fa fa-fw fa-spinner fa-pulse"
checkedIcon="fa fa-fw fa-check-square-o"
uncheckedIcon="fa fa-fw fa-square-o"
selectedIcon=""
partiallyCheckedIcon="fa fa-fw fa-check-square"
preventDeselect
data={nodes}
showCheckbox={isMulti}
callbacks={{ onDataChange, lazyLoad }}
{...props}
/>
);
};

TreeViewBase.propTypes = {
actionMapper: PropTypes.any,
loadData: PropTypes.func.isRequired,
lazyLoadData: PropTypes.func,
isMulti: PropTypes.bool,
check: PropTypes.func,
select: PropTypes.func,
};

TreeViewBase.defaultProps = {
actionMapper: {},
lazyLoadData: () => undefined,
isMulti: false,
check: undefined,
select: undefined,
};

export default TreeViewBase;
110 changes: 31 additions & 79 deletions app/javascript/components/tree-view/field.jsx
Original file line number Diff line number Diff line change
@@ -1,100 +1,40 @@
import React, { useState, useEffect } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import {Tree, ActionTypes } from 'react-wooden-tree';
import { Tree, ActionTypes } from 'react-wooden-tree';
import {
ControlLabel, FieldLevelHelp, FormGroup, HelpBlock,
} from 'patternfly-react';
import { useFieldApi, useFormApi } from '@data-driven-forms/react-form-renderer';

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

const setChecked = (keys = []) => ({ state, ...node }) => ({ ...node, state: { ...state, checked: keys.includes(node.attr.key) } });

const TreeViewField = ({ loadData, lazyLoadData, ...props }) => {
const [{ nodes }, setState] = useState({});

const { input: { value }, meta } = useFieldApi(props);
const TreeViewField = ({
loadData, lazyLoadData, helperText, isRequired, label, identifier, ...props
}) => {
const { input: { value = [], name }, meta } = useFieldApi(props);
const formOptions = useFormApi();

useEffect(() => {
loadData().then((values) => {
setState(state => ({ ...state, nodes: flatten(convert(values, setChecked(value))) }));
});
}, [loadData]);

const actionMapper = {
[ActionTypes.EXPANDED]: Tree.nodeExpanded,
[ActionTypes.CHECKED]: Tree.nodeChecked,
[ActionTypes.DISABLED]: Tree.nodeDisabled,
[ActionTypes.SELECTED]: Tree.nodeSelected,
[ActionTypes.LOADING]: Tree.nodeLoading,
[ActionTypes.CHILD_NODES]: (node, value) => {
// There's a bug in the react-wooden-tree, the passed value here contains all the child
// nodes and that would force the tree to mount nested children to the parent node twice.
// For now we're filtering out the child nodes manually by counting the number of dots in
// their identifier.
const min = value.reduce((min, key) => {
const nl = key.split('.').length;
return nl > min ? min : nl;
}, undefined);

return Tree.nodeChildren(node, value.filter(key => key.split('.').length === min));
},
[ActionTypes.CHECKED_DIRECTLY]: (node, value) => {
const values = formOptions.getFieldState(props.name).value;
formOptions.change(props.name, value ? [...values, node.attr.key] : values.filter(item => item !== node.attr.key));

const { value: values = [] } = formOptions.getFieldState(name);
formOptions.change(name, value ? [...values, identifier(node)] : values.filter(item => item !== identifier(node)));
return Tree.nodeChecked(node, value);
},
};

const lazyLoad = node => lazyLoadData(node).then((result) => {
const data = flatten(convert(result, setChecked(value)));

let subtree = {};
Object.keys(data).forEach((key) => {
if (key !== '') {
// Creating the node id from the parent id.
const nodeId = `${node.nodeId}.${key}`;
// Updating the children ids, so it does not point to something else.
const element = { ...data[key], nodeId, nodes: data[key].nodes.map(child => `${node.nodeId}.${child}`) };
subtree = { ...subtree, [nodeId]: element };
}
});
return subtree;
});

const onDataChange = commands => setState(state => ({
...state,
nodes: commands.reduce(
(nodes, { type, value, nodeId }) => (
type === ActionTypes.ADD_NODES
? Tree.addNodes(nodes, value)
: Tree.nodeUpdater(nodes, actionMapper[type](Tree.nodeSelector(nodes, nodeId), value))
), nodes,
),
}));

return (
<FormGroup validationState={meta.error ? 'error' : null}>
<ControlLabel>
{props.isRequired ? <RequiredLabel label={props.label} /> : props.label }
{props.helperText && <FieldLevelHelp content={props.helperText} />}
{isRequired ? <RequiredLabel label={label} /> : label }
{helperText && <FieldLevelHelp content={helperText} />}
</ControlLabel>
<Tree
data={nodes}
nodeIcon=""
expandIcon="fa fa-fw fa-angle-right"
collapseIcon="fa fa-fw fa-angle-down"
loadingIcon="fa fa-fw fa-spinner fa-pulse"
checkedIcon="fa fa-fw fa-check-square-o"
uncheckedIcon="fa fa-fw fa-square-o"
selectedIcon=""
partiallyCheckedIcon="fa fa-fw fa-check-square"
preventDeselect
showCheckbox
callbacks={{ onDataChange, lazyLoad }}
<TreeViewBase
loadData={loadData}
lazyLoadData={lazyLoadData}
actionMapper={actionMapper}
check={node => value.includes(identifier(node))}
isMulti
{...props}
/>
{meta.error && <HelpBlock>{ meta.error }</HelpBlock>}
Expand All @@ -103,8 +43,20 @@ const TreeViewField = ({ loadData, lazyLoadData, ...props }) => {
};

TreeViewField.propTypes = {
loadData: PropTypes.func,
loadData: PropTypes.func.isRequired,
lazyLoadData: PropTypes.func,
helperText: PropTypes.string,
isRequired: PropTypes.bool,
label: PropTypes.string,
identifier: PropTypes.func,
};

TreeViewField.defaultProps = {
lazyLoadData: () => undefined,
helperText: undefined,
isRequired: false,
label: undefined,
identifier: node => node.attr.key,
};

export default TreeViewField;
19 changes: 16 additions & 3 deletions app/javascript/components/tree-view/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Tree } from 'react-wooden-tree';
* Sets undefined checkbox to null.
* Moves the key to attr.key.
*/
const convert = (tree, fn) => tree.map((n) => {
const normalize = (tree, fn) => tree.map((n) => {
let node = { ...n };
if (node.class) {
node = { ...node, classes: node.class };
Expand All @@ -26,14 +26,27 @@ const convert = (tree, fn) => tree.map((n) => {
}

if (node.nodes) {
node = { ...node, nodes: convert(node.nodes, fn) };
node = { ...node, nodes: normalize(node.nodes, fn) };
}

return fn ? fn(node) : node;
});

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

// This function adjusts the nodes generated by the TreeBuilder for the usage with TreeView
const convert = (data, { check, select } = {}) => flatten(normalize(
data,
({ state: { checked, selected, ...state } = {}, ...node }) => ({
...node,
state: {
...state,
checked: check ? check(node) : checked,
selected: select ? select(node) : selected,
},
}),
));

/**
* Helper
* Activates the first active node in the tree, which is passed trought the props.
Expand All @@ -54,4 +67,4 @@ const activateNode = (tree, doActivate, key) => {

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

export { convert, flatten, activateNode, callBack };
export { convert, activateNode, callBack };
6 changes: 5 additions & 1 deletion app/javascript/components/tree-view/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
export { default as HierarchicalTreeView } from './hierarchical-tree-view';
import TreeViewRedux from './redux';
import TreeViewField from './field';
import TreeViewSelector from './selector';

export { TreeViewRedux, TreeViewField, TreeViewSelector };
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import { combineReducers } from '../../helpers/redux';
import reducers, { ACTIONS } from './reducers/index';
import basicStore from './reducers/basicStore';
import {
convert, callBack, activateNode, flatten,
convert, callBack, activateNode,
} from './helpers';

const HierarchicalTreeView = (props) => {
const TreeView = (props) => {
const {
tree_name,
bs_tree,
Expand Down Expand Up @@ -59,7 +59,7 @@ const HierarchicalTreeView = (props) => {
*/
useEffect(() => {
// FIXME - When the conversion wont be needed hopefuly in the future
const tree = activateNode(flatten(convert(JSON.parse(bs_tree))), silent_activate, select_node);
const tree = activateNode(convert(JSON.parse(bs_tree), node => node.state.checked, node => node.state.selected), silent_activate, select_node);

callBack(null, ACTIONS.EMPTY_TREE, null, namespace);
callBack(null, ACTIONS.ADD_NODES, tree, namespace);
Expand All @@ -81,7 +81,7 @@ const HierarchicalTreeView = (props) => {
tree: tree_name,
mode: 'all',
}).then((result) => {
const data = flatten(convert(result));
const data = convert(result);
let subtree = {};
Object.keys(data).forEach((key) => {
if (key !== '') {
Expand Down Expand Up @@ -117,7 +117,7 @@ const HierarchicalTreeView = (props) => {
);
};

HierarchicalTreeView.propTypes = {
TreeView.propTypes = {
tree_name: PropTypes.string.isRequired,
bs_tree: PropTypes.string.isRequired,
checkboxes: PropTypes.bool,
Expand All @@ -130,7 +130,7 @@ HierarchicalTreeView.propTypes = {
hierarchical_check: PropTypes.bool,
};

HierarchicalTreeView.defaultProps = {
TreeView.defaultProps = {
checkboxes: false,
allow_reselect: false,
oncheck: undefined,
Expand All @@ -140,6 +140,6 @@ HierarchicalTreeView.defaultProps = {
hierarchical_check: false,
};

const HierarchicalTreeViewConn = connect(null, { callBack })(HierarchicalTreeView);
const TreeViewRedux = connect(null, { callBack })(TreeView);

export default HierarchicalTreeViewConn;
export default TreeViewRedux;
Loading