terriajs
Version:
Geospatial data visualization platform.
169 lines (152 loc) • 6.83 kB
JSX
;
import flattenNested from '../../../../Core/flattenNested';
import ActiveConcept from './ActiveConcept';
import OpenInactiveConcept from './OpenInactiveConcept';
import Icon from '../../../Icon.jsx';
import ObserveModelMixin from '../../../ObserveModelMixin';
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import Styles from './summary-concept.scss';
const ADD_FIRST_TEXT = 'Add a condition';
const ADD_MORE_TEXT = 'Add new condition';
/*
* SummaryConcept displays all the active and open nodes under a given
* SummaryConcept.
* It has two "modes":
* When summaryConcept.allowMultiple is false, it can be substituted directly for
* a regular displayVariableConcept (or array of concepts). Each child can have
* zero, one or more items selected or not selected (depending on their allowMultiple setting).
* When summaryConcept.allowMultiple is true, it treats the child concepts as
* "conditions", any number of which can be added or removed.
*
* Parents containing 1 or more active nodes are shown via <./ActiveConcept>.
* (They may be open or closed, and ActiveConcept handles the difference.)
* Open nodes not containing any active nodes are shown via <./OpenInactiveConcept>.
* (This is typically the case when a user has pressed the AddButton but yet to
* activate any leaf nodes.)
* (If summaryConcept.allowMultiple is false, you cannot 'cancel' or 'go back' on this.)
* If summaryConcept.allowMultiple is true, then an <./AddButton> is also shown,
* which simply opens the root concept, at which point OpenInactiveConcept takes over.
*
* This design would need revision to handle concepts whose direct children are a mix of
* both leaf nodes and parent nodes.
*/
const SummaryConcept = createReactClass({
displayName: 'SummaryConcept',
mixins: [ObserveModelMixin],
propTypes: {
concept: PropTypes.object.isRequired, // Must be a SummaryConcept.
isLoading: PropTypes.bool
},
render() {
const concept = this.props.concept;
// Leaf nodes have either an undefined or a 0-length `items` array.
const isLeafNode = concept => (!concept.items || concept.items.length === 0);
const activeLeafNodes = concept.getNodes(isLeafNode).filter(concept => concept.isActive);
const activeLeafNodesByParent = groupByParentId(activeLeafNodes, parent => parent.id);
const openDescendantsWithoutActiveChildren = getOpenDescendantsWithoutActiveChildren(concept);
const isLoading = this.props.isLoading;
return (
<div className={Styles.root}>
<div className={Styles.title}>{concept.name}:</div>
<For each="group" index="i" of={activeLeafNodesByParent}>
<ActiveConcept key={i} rootConcept={concept} activeLeafNodesWithParent={group} isLoading={isLoading}/>
</For>
<If condition={activeLeafNodesByParent.length === 0 && openDescendantsWithoutActiveChildren.length === 0}>
<div className={Styles.noConditions}>
None
</div>
</If>
<If condition={openDescendantsWithoutActiveChildren.length > 0 && !isLoading}>
<OpenInactiveConcept rootConcept={concept} openInactiveConcept={openDescendantsWithoutActiveChildren[0]}/>
</If>
<If condition={concept.allowMultiple && openDescendantsWithoutActiveChildren.length === 0}>
<AddButton rootConcept={concept} numberOfExisting={activeLeafNodesByParent.length}/>
</If>
</div>
);
},
});
/**
* We only want to show an <OpenInactiveConcept> if there is an open item without any active items in it.
* This will return a flat array of any such concepts.
* @param {Concept} concept [description]
* @return {Array} A nested array of open concepts.
*/
function getOpenDescendantsWithoutActiveChildren(concept) {
const openDescendants = getOpenDescendants(concept);
const flattenedOpenDescendants = flattenNested(openDescendants);
return flattenedOpenDescendants.filter(hasNoActiveChildren);
}
/**
* Returns a nested array of the open descendants of this concept (including itself).
* If an open concept itself has open descendants, they are ignored.
* @param {Concept} concept [description]
* @return {Array} A nested array of open concepts.
*/
function getOpenDescendants(concept) {
if (concept.isOpen) {
return [concept];
}
if (!concept.items) {
return [];
}
return concept.items.map(child => getOpenDescendants(child));
}
/**
* @param {Concept} concept.
* @return {Boolean} Does this concept have no active children?
*/
function hasNoActiveChildren(concept) {
return !concept.items || concept.items.every(child => !child.isActive);
}
/**
* Returns an array which groups all the nodes with the same parent id into separate sub-arrays.
* @param {Object[]} nodes An array of objects with a 'parent' property.
* @param {groupByParentId~idFunction} idFunction A function which gets the id of a parent.
* @return {Object[]} An array of objects with keys parent, children.
* @private
*/
function groupByParentId(nodes, idFunction) {
const results = {};
nodes.forEach(node => {
const id = idFunction(node.parent);
if (!results[id]) {
results[id] = {parent: node.parent, children: []};
}
results[id].children.push(node);
});
return Object.keys(results).map(key => results[key]);
}
/**
* Function that is called to find the id of a parent.
* Eg. parent => parent.id.
* @callback groupByParentId~idFunction
* @param {Object} parent A parent.
* @return {String} The parent id.
*/
const AddButton = createReactClass({
displayName: 'AddButton',
mixins: [ObserveModelMixin],
propTypes: {
rootConcept: PropTypes.object.isRequired,
numberOfExisting: PropTypes.number
},
addNew() {
this.props.rootConcept.closeDescendants();
this.props.rootConcept.isOpen = true;
},
render() {
const addText = (this.props.numberOfExisting > 0) ? ADD_MORE_TEXT : ADD_FIRST_TEXT;
return (
<div className={Styles.section}>
<button onClick={this.addNew} className={Styles.btnAddNew}>
<Icon glyph={Icon.GLYPHS.add}/>
<span className={Styles.text}>{addText}</span>
</button>
</div>
);
},
});
module.exports = SummaryConcept;