diff --git a/components/General/FaramList/FaramListApi.js b/components/General/FaramList/FaramListApi.js
index 46d46f470..cea5c87cf 100644
--- a/components/General/FaramList/FaramListApi.js
+++ b/components/General/FaramList/FaramListApi.js
@@ -76,8 +76,12 @@ export default class FaramListApi extends FaramGroupApi {
return this.onClickMemory[faramElementName];
}
- const newOnClick = () => {
- const newValue = faramAction(this.props.value || emptyArray, faramElementName);
+ const newOnClick = (clickParams) => {
+ const newValue = faramAction(
+ this.props.value || emptyArray,
+ faramElementName,
+ clickParams,
+ );
// Button doesn't have children, so no need to propagate faramInfo
this.props.onChange(newValue);
};
diff --git a/components/Input/TreeSelection/ExtraRoot.js b/components/Input/TreeSelection/ExtraRoot.js
new file mode 100644
index 000000000..f002a714d
--- /dev/null
+++ b/components/Input/TreeSelection/ExtraRoot.js
@@ -0,0 +1,79 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import hoistNonReactStatics from 'hoist-non-react-statics';
+
+
+const propTypes = {
+ withRoot: PropTypes.bool,
+ rootKey: PropTypes.string,
+ rootTitle: PropTypes.string,
+ rootSelectedInitial: PropTypes.bool,
+ value: PropTypes.arrayOf(PropTypes.object),
+ onChange: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+ withRoot: false,
+ rootKey: undefined,
+ rootTitle: undefined,
+ rootSelectedInitial: undefined,
+ value: [],
+};
+
+
+export default (WrappedComponent) => {
+ class ComponentWithExtraRoot extends React.PureComponent {
+ static propTypes = propTypes;
+ static defaultProps = defaultProps;
+
+ constructor(props) {
+ super(props);
+ this.lastRootSelectionValue = props.rootSelectedInitial;
+ }
+
+ handleChange = (value) => {
+ const { onChange } = this.props;
+ this.lastRootSelectionValue = value[0].selected;
+ onChange(value[0].nodes);
+ }
+
+ calcProps = () => {
+ const {
+ withRoot,
+ rootKey,
+ rootTitle,
+ value,
+ ...otherProps
+ } = this.props;
+
+ if (!withRoot) {
+ return this.props;
+ }
+
+ return {
+ ...otherProps,
+ initialExpandState: { [rootKey]: true },
+ value: [{
+ key: rootKey,
+ title: rootTitle,
+ selected: this.lastRootSelectionValue || false,
+ nodes: value,
+ draggable: false,
+ }],
+ onChange: this.handleChange,
+ };
+ }
+
+ render() {
+ const props = this.calcProps();
+ return (
+
+ );
+ }
+ }
+
+ return hoistNonReactStatics(
+ ComponentWithExtraRoot,
+ WrappedComponent,
+ );
+};
diff --git a/components/Input/TreeSelection/Select.js b/components/Input/TreeSelection/Select.js
new file mode 100644
index 000000000..59554052a
--- /dev/null
+++ b/components/Input/TreeSelection/Select.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import hoistNonReactStatics from 'hoist-non-react-statics';
+
+
+const selectors = {
+ labelSelector: 'title',
+ keySelector: 'key',
+ nodesSelector: 'nodes',
+};
+
+export default (WrappedComponent) => {
+ class SelectedComponent extends React.PureComponent {
+ static calcNewData = (data, props) => {
+ const newData = Object.keys(selectors)
+ .filter(s => props[s])
+ .reduce((acc, selector) => ({
+ ...acc,
+ [selectors[selector]]: props[selector](data),
+ }), { ...data });
+
+ if (newData.nodes) {
+ newData.nodes =
+ newData.nodes.map(d => SelectedComponent.calcNewData(d, props));
+ }
+
+ return newData;
+ }
+
+ calcProps = () => {
+ const { data } = this.props;
+ const newData = data &&
+ data.map(datum => SelectedComponent.calcNewData(datum, this.props));
+
+ return {
+ ...this.props,
+ data: newData,
+ };
+ }
+
+ render() {
+ const props = this.calcProps();
+ return (
+
+ );
+ }
+ }
+
+ return hoistNonReactStatics(
+ SelectedComponent,
+ WrappedComponent,
+ );
+};
diff --git a/components/Input/TreeSelection/SeparateDataValue.js b/components/Input/TreeSelection/SeparateDataValue.js
new file mode 100644
index 000000000..aeee611c8
--- /dev/null
+++ b/components/Input/TreeSelection/SeparateDataValue.js
@@ -0,0 +1,91 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import hoistNonReactStatics from 'hoist-non-react-statics';
+import { pick } from '../../../utils/common';
+
+const emptyObject = {};
+
+const propTypes = {
+ data: PropTypes.arrayOf(PropTypes.object).isRequired,
+ value: PropTypes.objectOf(PropTypes.shape({
+ selected: PropTypes.oneOf([true, false, 'fuzzy']),
+ nodes: PropTypes.objectOf(PropTypes.object),
+ })),
+ onChange: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+ data: [],
+ value: {},
+};
+
+const mergeDataValue = (data = {}, value = {}) => {
+ const newValue = {
+ ...data,
+ selected: value.selected || false,
+ };
+
+ if (newValue.nodes) {
+ newValue.nodes = newValue.nodes.map(datum => mergeDataValue(
+ datum,
+ (value.nodes || emptyObject)[datum.key],
+ ));
+ }
+
+ return newValue;
+};
+
+
+const pickRecursive = (obj, keys) => {
+ const pickedData = pick(obj, keys);
+ if (pickedData.nodes) {
+ pickedData.nodes = pickedData.nodes.reduce((acc, d) => ({
+ ...acc,
+ [d.key]: pickRecursive(d, keys),
+ }), {});
+ }
+ return pickedData;
+};
+
+export default (WrappedComponent) => {
+ class SeparatedComponent extends React.PureComponent {
+ static propTypes = propTypes;
+ static defaultProps = defaultProps;
+
+ handleChange = (value) => {
+ const { onChange } = this.props;
+ const newValue = value.reduce((acc, d) => ({
+ ...acc,
+ [d.key]: pickRecursive(d, ['selected', 'nodes']),
+ }), {});
+ onChange(newValue);
+ }
+
+ calcProps = () => {
+ const { value, data, ...otherProps } = this.props;
+
+ const newValue = data.map(datum => mergeDataValue(
+ datum,
+ (value || emptyObject)[datum.key],
+ ));
+
+ return {
+ ...otherProps,
+ value: newValue,
+ onChange: this.handleChange,
+ };
+ }
+
+ render() {
+ const props = this.calcProps();
+ return (
+
+ );
+ }
+ }
+
+ return hoistNonReactStatics(
+ SeparatedComponent,
+ WrappedComponent,
+ );
+};
diff --git a/components/Input/TreeSelection/index.js b/components/Input/TreeSelection/index.js
index 74c31b0b8..5a1b74f12 100644
--- a/components/Input/TreeSelection/index.js
+++ b/components/Input/TreeSelection/index.js
@@ -10,27 +10,33 @@ import {
import { iconNames } from '../../../constants';
import Button from '../../Action/Button';
import { FaramInputElement } from '../../General/FaramElements';
+import Select from './Select';
+import ExtraRoot from './ExtraRoot';
+import SeparateDataValue from './SeparateDataValue';
-// FIXME: don't use globals
-// eslint-disable-next-line no-unused-vars
import styles from './styles.scss';
+
+const noOp = () => undefined;
+
const propTypes = {
className: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string,
title: PropTypes.string,
- selected: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
- nodes: PropTypes.arrayOf(PropTypes.object), // Children nodes
+ nodes: PropTypes.arrayOf(PropTypes.object),
+ selected: PropTypes.oneOf([true, false, 'fuzzy']),
draggable: PropTypes.bool,
})),
onChange: PropTypes.func,
+ initialExpandState: PropTypes.shape({}),
};
const defaultProps = {
className: '',
- onChange: undefined,
+ onChange: noOp,
value: [],
+ initialExpandState: {},
};
const DragHandle = SortableHandle(() => (
@@ -39,7 +45,7 @@ const DragHandle = SortableHandle(() => (
// Get cumulative selected state from a list of nodes
-function getSelectedState(nodes) {
+const getSelectedState = (nodes) => {
let selected = true;
// If any one child is in fuzzy state, we are in fuzzy state
@@ -59,22 +65,17 @@ function getSelectedState(nodes) {
}
return selected;
-}
-
-
-// Update selected state of a node based on its
-// children recursively
-function updateNodeState(node) {
- const nodes = node.nodes && node.nodes.map(n => updateNodeState(n));
+};
- return {
- ...node,
- nodes,
- selected: nodes ? getSelectedState(nodes) : node.selected,
- };
-}
+// Set selected state for a particular node where selected = true/false/'fuzzy'
+// and do it for all the children.
+const setNodeSelection = (node, selected) => ({
+ ...node,
+ nodes: node.nodes && node.nodes.map(n => setNodeSelection(n, selected)),
+ selected,
+});
-class TreeSelection extends React.PureComponent {
+class NormalTreeSelection extends React.PureComponent {
static propTypes = propTypes;
static defaultProps = defaultProps;
@@ -82,17 +83,10 @@ class TreeSelection extends React.PureComponent {
super(props);
this.state = {
- expanded: {},
- value: this.createValue(props.value),
+ expanded: props.initialExpandState,
};
}
- componentWillReceiveProps(nextProps) {
- if (nextProps.value !== this.state.value) {
- this.setState({ value: this.createValue(nextProps.value) });
- }
- }
-
// Based on selected values (true/false/'fuzzy'), get class-name
// for checkbox
getCheckBoxStyle = (selected) => {
@@ -110,17 +104,6 @@ class TreeSelection extends React.PureComponent {
return classNames.join(' ');
}
- // Set selected state for a particular node where selected = true/false/'fuzzy'
- // and do it for all the children
- // Immutable operation: so returns new object
- setNodeSelection = (node, selected) => ({
- ...node,
- nodes: node.nodes && node.nodes.map(n => this.setNodeSelection(n, selected)),
- selected,
- })
-
- createValue = value => value.map(v => updateNodeState(v))
-
// Toggle expand state of a node
handleToggleExpand = (key) => {
const expanded = { ...this.state.expanded };
@@ -130,13 +113,11 @@ class TreeSelection extends React.PureComponent {
// Handle toggling the state of checkbox including its children
handleCheckBox = (key) => {
- const value = [...this.state.value];
+ const value = [...this.props.value];
const index = value.findIndex(v => v.key === key);
const state = !value[index].selected;
- value[index] = this.setNodeSelection(value[index], state);
-
- this.setState({ value });
+ value[index] = setNodeSelection(value[index], state);
if (this.props.onChange) {
this.props.onChange(value);
@@ -146,7 +127,7 @@ class TreeSelection extends React.PureComponent {
// Update the children nodes
// Change may include selected state and order of the children
handleChildrenChange = (key, nodes) => {
- const value = [...this.state.value];
+ const value = [...this.props.value];
const index = value.findIndex(v => v.key === key);
const selected = getSelectedState(nodes);
@@ -158,7 +139,6 @@ class TreeSelection extends React.PureComponent {
};
value[index] = nodeValue;
- this.setState({ value });
if (this.props.onChange) {
this.props.onChange(value);
@@ -168,11 +148,7 @@ class TreeSelection extends React.PureComponent {
// Start sortable stuffs
handleSortEnd = ({ oldIndex, newIndex }) => {
- const value = arrayMove(this.state.value, oldIndex, newIndex);
- this.setState({
- value,
- });
-
+ const value = arrayMove(this.props.value, oldIndex, newIndex);
if (this.props.onChange) {
this.props.onChange(value);
}
@@ -180,7 +156,7 @@ class TreeSelection extends React.PureComponent {
SortableNode = SortableElement(({ value }) => this.renderNode(value))
- SortableTree = SortableContainer(({ items }) => (
+ SortableTree = SortableContainer(({ items = [] }) => (
{items.map((node, index) => (
)}
- );
+ )
render() {
- const { className } = this.props;
- const { value } = this.state;
+ const { className, value } = this.props;
const classNames = [
className,
styles.treeSelection,
@@ -248,19 +223,21 @@ class TreeSelection extends React.PureComponent {
return (
- {value && (
-
- )}
+
);
}
}
+const TreeSelection = ExtraRoot(NormalTreeSelection);
export default FaramInputElement(TreeSelection);
+export const SeparatedTreeSelection = FaramInputElement(SeparateDataValue(TreeSelection));
+export const TreeSelectionWithSelectors =
+ FaramInputElement(Select(SeparateDataValue(TreeSelection)));
diff --git a/components/Input/TreeSelection/styles.scss b/components/Input/TreeSelection/styles.scss
index aed8ecc97..e89b402c5 100644
--- a/components/Input/TreeSelection/styles.scss
+++ b/components/Input/TreeSelection/styles.scss
@@ -21,10 +21,6 @@
padding: $spacing-extra-small;
color: $color-accent;
font-size: $font-size-medium-alt;
-
- &.unchecked {
- color: $color-text;
- }
}
.node-title {
@@ -48,10 +44,8 @@
}
}
-:global {
- body {
- >.tree-node {
- @include shadow-small;
- }
+body {
+ >.tree-node {
+ @include shadow-small;
}
}