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; } }