labo-components
Version:
393 lines (362 loc) • 15.5 kB
JSX
import React from 'react';
import PropTypes from 'prop-types';
import AggregationCreator from './AggregationCreator';
import FlexModal from '../FlexModal';
import IDUtil from '../../util/IDUtil';
import ComponentUtil from '../../util/ComponentUtil';
import ReactTooltip from 'react-tooltip';
import classNames from 'classnames';
import AggregationBox from './AggregationBox';
//this component draws the aggregations (a.k.a. facets) and merely outputs
//the user selections to the parent component
export default class AggregationList extends React.Component {
constructor(props) {
super(props);
this.state = {
showModal: false,
showModalWarning: false,
//TODO merge these three states into one, since they all keep state information per aggregation box
showAllModes: {}
};
this.CLASS_PREFIX = "agl";
this.minToShow = 5;
this.currentFacet = null;
}
//communicates the selected facets back to the parent component
onOutput = (desiredFacets, selectedFacets) => {
if(!selectedFacets) {
selectedFacets = this.props.selectedFacets;
}
if (this.props.onOutput) {
this.props.onOutput(this.constructor.name, {
desiredFacets: desiredFacets,
selectedFacets: selectedFacets
});
}
};
onComponentOutput = (componentClass, data) => {
if (componentClass === "AggregationCreator" && data) {
const desiredFacets = this.props.desiredFacets;
desiredFacets.unshift(data);
this.onOutput(desiredFacets, this.props.selectedFacets);
ComponentUtil.hideModal(
this,
"showModal",
"field_select__modal",
true
);
}
};
/* ------------------------------------- FACET SELECTION -------------------------------- */
toggleSelectedFacet = (key, value) => {
const facets = this.props.selectedFacets;
if (facets) {
if (facets[key]) {
const index = facets[key].indexOf(value);
if (index === -1) {
facets[key].push(value); //add the value
} else {
facets[key].splice(index, 1); // remove the value
if (facets[key].length === 0) {
delete facets[key];
}
}
} else {
facets[key] = [value];
}
//output to the parent component
this.onOutput(this.props.desiredFacets, facets);
}
};
/*------------------------------------- REMOVE DIALOG (TODO MAKE NICER) ----------------------------*/
showRemoveDialog = (key, index) => {
this.currentFacet = key;
//FIXME this part is still nasty, but for now it's necessary to prevent
//toggling the header menu when clicking the "X"
if (document.querySelector("#index__" + index)) {
document.querySelector("#index__" + index).addEventListener(
"click",
function(event) {
event.preventDefault();
},
{ once: true }
);
ComponentUtil.showModal(
this,
"showModalWarning",
"field_select_facet__modal",
true
);
}
};
removeAggregation = () => {
//first remove the entry from the desiredFacets
const desiredFacets = this.props.desiredFacets;
for (let i = desiredFacets.length - 1; i >= 0; i--) {
if (desiredFacets[i].field === this.currentFacet) {
desiredFacets.splice(i, 1);
break;
}
}
//then throw away any selected value from the selectedFacets
if (this.props.selectedFacets) {
delete this.props.selectedFacets[this.currentFacet];
}
ComponentUtil.hideModal(
this,
"showModalWarning",
"field_select_facet__modal",
true
);
this.onOutput(desiredFacets, this.props.selectedFacets);
};
//makes sure to filter out heavy facets that will freeze the screen
//(in case the search term is smaller than 3 characters long)
filterHeavyFacets = (desiredFacets, allowHeavyFacets) => {
const lightFacets = this.props.collectionConfig.getFacetSelectionList(false).map(f => f.value);
return allowHeavyFacets ? desiredFacets : desiredFacets.filter(df => lightFacets.indexOf(df.field) !== -1)
};
/*------------------------------------- FUNCTION FOR GENERATING UI FRIENDLY DATA OBJECT --------------*/
//returns render friendly object based on the data supplied in the props
generateUIData = () => {
//will be ultimately returned containing a list of ui data per "desired aggregation"
const uiData = [];
const desiredFacets = this.props.desiredFacets ?
this.filterHeavyFacets(this.props.desiredFacets, this.props.allowHeavyFacets) :
[];
//Check if all selected facets are in the desired aggregation list, if not, add doc_count 0
Object.keys(this.props.selectedFacets).forEach(field => {
this.props.selectedFacets[field].forEach(facetValue => {
const found = this.props.aggregations[field].find(
aggr => aggr["key"] === facetValue
);
if (!found) {
this.props.aggregations[field].push({
key: facetValue,
doc_count: 0
});
}
});
});
//loop through the desired facets, available in the state
desiredFacets.forEach((da, index) => {
// skip the date_histogram types, as they shouldn't be converted to a list
// however, they should be included in this loop in order to keep the facet
// index intact
if (da.type === "date_histogram") {
return;
}
//first check if the aggregation has anything in it
const isEmptyAggr = !(
this.props.aggregations[da.field] &&
this.props.aggregations[da.field].length > 0
);
//then parse the retrieved facets/buckets
let facets = [];
if (!isEmptyAggr) {
facets = this.props.aggregations[da.field].map(facet => {
return {
key: facet.key,
guid: da.field + "|" + facet.key,
count: facet.doc_count,
selected: this.props.selectedFacets[da.field] && this.props.selectedFacets[da.field].indexOf(
facet.key
) !== -1
};
});
// move the selected facets to the top of the array, so they always appear on top in the list
const selectedFacets = facets.filter(f => f.selected);
facets = facets.filter(f => !f.selected);
facets.unshift(...selectedFacets);
}
//then add them to the convenient UI object (together with the exclusion property)
uiData.push({
facets: facets,
exclude: da.exclude === undefined ? false : da.exclude,
field: da.field,
title:// show user generated title or else the automated prettified title
da.title ||
this.props.collectionConfig.toPrettyFieldName(da.field),
empty: isEmptyAggr, //does the aggregation have anything in it
index: index, // temporarily needed for guid
guid: "facets__" + index
});
});
return uiData;
};
/*------------------------------------- FUNCTIONS FOR RENDERING ----------------------------*/
renderEmptyBlocks = aggr => (
<div className={IDUtil.cssClassName('empty-facet-block', this.CLASS_PREFIX)}
key={"facet__" + aggr.index}
id={"index__" + aggr.index}>
<div className={IDUtil.cssClassName('title-wrapper', this.CLASS_PREFIX)}>
<i
className='fas fa-info-circle'
data-for={'tooltip__' + aggr.index}
data-tip={aggr.field}
data-html={true}
/>
<div className={IDUtil.cssClassName('facet-title', this.CLASS_PREFIX)}>
(0) {aggr.title}{" "}
</div>
<div
className='fas fa-times'
onClick={this.showRemoveDialog.bind(
this,
aggr.field,
aggr.index
)}
/>
</div>
<ReactTooltip id={"tooltip__" + aggr.index}/>
</div>
);
renderSelectedFacet = (curAggr, f) => {
let title = f.key + ' (' + curAggr.title + ')';
let count = f.count;
if (curAggr.exclude === true) {
title = "NOT - " + title;
count = 0;
}
return (
<div key={'__sf__' + curAggr.field + f.key} className={classNames(
{ exclude: curAggr.exclude === true },
IDUtil.cssClassName('selected-item', this.CLASS_PREFIX)
)}>
<span className="elem-label" title={title}>{title}{" "}</span>
<span className={IDUtil.cssClassName('count', this.CLASS_PREFIX)}>
{count}
</span>
<span className="fas fa-times" onClick={this.toggleSelectedFacet.bind(this, curAggr.field, f.key)}/>
</div>
);
};
renderNewFacetModal = (allowHeavyFacets, fieldList, desiredFacets) => {
const filteredFields = fieldList.filter(
item => desiredFacets.findIndex(facet => facet.field === item.value) === -1
);
return (
<FlexModal
size="large"
elementId="field_select__modal"
stateVariable="showModal"
owner={this}
title="Create a new facet"
>
<AggregationCreator
allowHeavyFacets={allowHeavyFacets}
onOutput={this.onComponentOutput}
key={this.props.searchId}
fieldList={filteredFields}
/>
</FlexModal>
);
};
renderRemoveFacetModal = () => (
<FlexModal
elementId="field_select_facet__modal"
stateVariable="showModalWarning"
owner={this}
title="Remove current facet?"
>
<div>
<p>
You are removing the current facet "
<u>{this.props.collectionConfig ?
this.props.collectionConfig.toPrettyFieldName(this.currentFacet) :
this.currentFacet
}</u>". You can bring it back by
using the "New" facet option and searching for the same
field name again
</p>
<br/>
<button type="button" onClick={this.removeAggregation} className="btn btn-primary">
Remove
</button>
</div>
</FlexModal>
);
render() {
//contains all required data for generating the (empty) aggregation blocks and selected facets
const uiData = this.generateUIData();
//modals
const aggregationCreatorModal = this.state.showModal && this.props.collectionConfig
? this.renderNewFacetModal(
this.props.allowHeavyFacets,
this.props.collectionConfig.getFacetSelectionList(this.props.allowHeavyFacets),
this.props.desiredFacets
)
: null;
const aggregationModalWarning = this.state.showModalWarning
? this.renderRemoveFacetModal()
: null;
//for each empty aggregation, add a (rendered) block to the list (of empty aggregations)
const emptyAggrBlocks = uiData
.filter(aggr => aggr.empty)
.map(this.renderEmptyBlocks); //contains aggregations without any results
const aggregationBlocks = [];
let selectedFacets = []; //holds the list of selected facets to be displayed at the top
//loop through the non-empty "desired aggregations" (non-histogram only)
uiData
.filter(aggr => !aggr.empty)
.forEach(curAggr => {
//add another (rendered) aggregation block to the list
aggregationBlocks.push(
<AggregationBox
key={this.props.searchId + '__' + curAggr.guid}
showRemoveDialog={this.showRemoveDialog}
onOutput={this.onOutput}
selectedFacets={this.props.selectedFacets}
desiredFacets={this.props.desiredFacets}
onToggleSelectedFacet={this.toggleSelectedFacet}
data={curAggr}
/>);
//add the (rendered) facets that are selected within the current aggregation
//block (to be displayed at the top)
selectedFacets = selectedFacets.concat(
curAggr.facets.filter(f => f.selected).map(
f => {return this.renderSelectedFacet(curAggr, f)}
)
);
});
//finally render the whole thing
return (
<div className={IDUtil.cssClassName("aggregation-list checkboxes")}>
{aggregationCreatorModal}
{aggregationModalWarning}
{/* Create new facet */}
<div className={IDUtil.cssClassName("tab-new", this.CLASS_PREFIX)}>
<button
className="btn"
onClick={ComponentUtil.showModal.bind(
this,
this,
"showModal"
)}
>
<i className="fas fa-plus" /> Add a new facet
</button>
</div>
{/* Selected/active facets */}
<div
className={IDUtil.cssClassName('selected-facets', this.CLASS_PREFIX)}>
{selectedFacets}
</div>
{/* Empty facets */}
{emptyAggrBlocks}
{/* Facet list */}
{aggregationBlocks}
</div>
);
}
}
AggregationList.propTypes = {
aggregations: PropTypes.object.isRequired,
collectionConfig: PropTypes.object.isRequired,
desiredFacets: PropTypes.array,
onOutput: PropTypes.func.isRequired,
queryId: PropTypes.string,
searchId: PropTypes.string,
allowHeavyFacets: PropTypes.bool.isRequired, //for determining whether to show heavy facets or not
selectedFacets: PropTypes.object
};