app/javascript/components/tree-view/redux.jsx
/* eslint camelcase: ["warn", {allow: ["bs_tree", "tree_name", "click_url", "check_url", "allow_reselect", "hierarchical_check", "silent_activate", "select_node"]}] */
import React, { useEffect, useCallback } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Tree, Node } from 'react-wooden-tree';
import { http } from '../../http_api';
import { combineReducers } from '../../helpers/redux';
import reducers, { ACTIONS } from './reducers/index';
import basicStore from './reducers/basicStore';
import {
convert, callBack, activateNode,
} from './helpers';
import { nodeCheckedWithDirty } from './reducers/helpers';
const TreeView = (props) => {
const {
tree_name,
bs_tree,
check_url,
click_url,
oncheck,
onclick,
checkboxes,
allow_reselect,
hierarchical_check,
callBack,
silent_activate,
select_node,
} = props;
const namespace = tree_name;
const ConnectedNode = connect((store, ownProps) => ({ ...store[namespace][ownProps.nodeId] }))(Node);
const ReduxTree = connect((store) => ({ data: { ...store[namespace] } }))(Tree);
const getData = useCallback(() => {
// FIXME: This check if the reducer exists is not ideal, but it is needed
// when the tree is rerendered without page change (Edit Report Menu's).
// When fixed the useEffect should remove the created reducer on unmount.
if (!ManageIQ.redux.store.getState()[namespace]) {
ManageIQ.redux.addReducer({
[namespace]: combineReducers([
basicStore,
reducers(oncheck, onclick),
], namespace),
});
}
}, []);
/**
* After the component mounts adds a tree specific reducer to the store
*/
useEffect(() => {
// Setting the URLs for the further actions:
ManageIQ.tree.checkUrl = check_url;
ManageIQ.tree.clickUrl = click_url;
getData();
}, []);
/**
* Populates the store from the prop by converting the supplied tree to
* the correct format and then dispatching it to the store.
*/
useEffect(() => {
// FIXME - When the conversion wont be needed hopefuly in the future
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);
}, [bs_tree]);
const onDataChange = (commands) => {
ManageIQ.tree.checkUrl = check_url;
ManageIQ.tree.clickUrl = click_url;
const tree = activateNode(convert(JSON.parse(bs_tree), (node) => node.state.checked, (node) =>
node.state.selected), silent_activate, select_node);
commands.forEach((command) => {
let node;
if (command.type === 'state.checked') {
node = Tree.nodeSelector(tree, command.nodeId);
const selectedNode = nodeCheckedWithDirty(node, command.value);
if (selectedNode.classes === 'dirty') {
ManageIQ.redux.addReducer({
[namespace]: combineReducers([
basicStore,
reducers(oncheck, onclick),
], namespace),
});
sendDataWithRx({ name: 'dirty' });
} else {
getData();
sendDataWithRx({ name: 'notdirty' });
}
}
callBack(command.nodeId, `@@tree/${command.type}`, command.value, namespace);
});
};
/**
* Lazy load function with a wrapper.
* The lazy load should return promise but we are preprocessing the data
* before passing to the component. For the indentitation to work we need to update the nodeId as
* well, not only the key in the array.
*
* FIXME: Remove wrapper after server returning flat trees.
*/
const lazyLoad = (node) => new Promise((resolve, reject) => {
http.post(`/${ManageIQ.controller}/tree_autoload`, {
id: node.attr.key,
tree: tree_name,
mode: 'all',
}).then((result) => {
const data = convert(result);
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 };
}
});
resolve(subtree);
}).catch((error) => reject(error));
});
return (
<ReduxTree
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"
connectedNode={ConnectedNode}
checkable={checkboxes}
showCheckbox={checkboxes}
allowReselect={allow_reselect}
callbacks={{ onDataChange, lazyLoad }}
hierarchicalCheck={hierarchical_check}
{...props}
/>
);
};
TreeView.propTypes = {
tree_name: PropTypes.string.isRequired,
bs_tree: PropTypes.string.isRequired,
checkboxes: PropTypes.bool,
allow_reselect: PropTypes.bool,
oncheck: PropTypes.string,
onclick: PropTypes.string,
check_url: PropTypes.string,
click_url: PropTypes.string,
callBack: PropTypes.func.isRequired,
hierarchical_check: PropTypes.bool,
silent_activate: PropTypes.bool || undefined,
select_node: PropTypes.string,
};
TreeView.defaultProps = {
checkboxes: false,
allow_reselect: false,
oncheck: undefined,
onclick: undefined,
check_url: '',
click_url: '',
hierarchical_check: false,
silent_activate: undefined,
select_node: undefined,
};
const TreeViewRedux = connect(null, { callBack })(TreeView);
export default TreeViewRedux;