appbaseio/reactivesearch

View on GitHub
packages/web/src/components/list/MultiDataList.js

Summary

Maintainability
F
1 wk
Test Coverage
import React, { Component } from 'react';

import {
    addComponent,
    removeComponent,
    watchComponent,
    updateQuery,
    setQueryListener,
    setQueryOptions,
} from '@appbaseio/reactivecore/lib/actions';
import {
    isEqual,
    checkValueChange,
    checkPropChange,
    checkSomePropChange,
    getClassName,
    pushToAndClause,
    getQueryOptions,
} from '@appbaseio/reactivecore/lib/utils/helper';

import types from '@appbaseio/reactivecore/lib/utils/types';

import Title from '../../styles/Title';
import Input from '../../styles/Input';
import Container from '../../styles/Container';
import { UL, Checkbox } from '../../styles/FormControlList';
import { connect } from '../../utils';
import { getAggsQuery } from './utils';

class MultiDataList extends Component {
    constructor(props) {
        super(props);

        this.state = {
            currentValue: {},
            searchTerm: '',
            options: props.data || [],
        };
        this.internalComponent = `${props.componentId}__internal`;
        this.type = 'term';
        this.locked = false;
        props.setQueryListener(props.componentId, props.onQueryChange, null);
    }

    componentWillMount() {
        this.props.addComponent(this.internalComponent);
        this.props.addComponent(this.props.componentId);

        if (this.props.showCount) {
            this.updateQueryOptions(this.props);
        }

        this.setReact(this.props);

        if (this.props.selectedValue.length) {
            this.setValue(this.props.selectedValue, true);
        } else if (this.props.defaultSelected) {
            this.setValue(this.props.defaultSelected, true);
        }
    }

    componentWillReceiveProps(nextProps) {
        checkPropChange(this.props.react, nextProps.react, () => this.setReact(nextProps));

        checkSomePropChange(this.props, nextProps, ['dataField', 'nestedField'], () => {
            this.updateQuery(Object.keys(this.state.currentValue), nextProps);

            if (nextProps.showCount) {
                this.updateQueryOptions(nextProps);
            }
        });

        checkPropChange(this.props.data, nextProps.data, () => {
            if (nextProps.showCount) {
                this.updateQueryOptions(nextProps);
            }
        });

        checkPropChange(this.props.options, nextProps.options, () => {
            this.updateStateOptions(nextProps.options[nextProps.dataField].buckets);
        });

        let selectedValue = Object.keys(this.state.currentValue);

        if (this.props.selectAllLabel) {
            selectedValue = selectedValue.filter(val => val !== this.props.selectAllLabel);

            if (this.state.currentValue[this.props.selectAllLabel]) {
                selectedValue = [this.props.selectAllLabel];
            }
        }

        if (!isEqual(this.props.defaultSelected, nextProps.defaultSelected)) {
            this.setValue(nextProps.defaultSelected, true);
        } else if (!isEqual(selectedValue, nextProps.selectedValue)) {
            this.setValue(nextProps.selectedValue, true);
        }
    }

    componentWillUnmount() {
        this.props.removeComponent(this.props.componentId);
        this.props.removeComponent(this.internalComponent);
    }

    setReact(props) {
        const { react } = this.props;

        if (react) {
            const newReact = pushToAndClause(react, this.internalComponent);
            props.watchComponent(props.componentId, newReact);
        } else {
            props.watchComponent(props.componentId, {
                and: this.internalComponent,
            });
        }
    }

    static defaultQuery = (value, props) => {
        let query = null;
        const type = props.queryFormat === 'or' ? 'terms' : 'term';
        if (
            props.selectAllLabel
            && Array.isArray(value)
            && value.includes(props.selectAllLabel)
        ) {
            query = {
                exists: {
                    field: props.dataField,
                },
            };
        } else if (value) {
            let listQuery;
            if (props.queryFormat === 'or') {
                listQuery = {
                    [type]: {
                        [props.dataField]: value,
                    },
                };
            } else {
                // adds a sub-query with must as an array of objects for each term/value
                const queryArray = value.map(item => ({
                    [type]: {
                        [props.dataField]: item,
                    },
                }));
                listQuery = {
                    bool: {
                        must: queryArray,
                    },
                };
            }

            query = value.length ? listQuery : null;
        }

        if (query && props.nestedField) {
            return {
                query: {
                    nested: {
                        path: props.nestedField,
                        query,
                    },
                },
            };
        }

        return query;
    };

    setValue = (value, isDefaultValue = false, props = this.props) => {
        // ignore state updates when component is locked
        if (props.beforeValueChange && this.locked) {
            return;
        }

        this.locked = true;
        const { selectAllLabel } = this.props;
        let { currentValue } = this.state;
        let finalValues = null;

        if (
            selectAllLabel
            && ((Array.isArray(value) && value.includes(selectAllLabel))
                || (typeof value === 'string' && value === selectAllLabel))
        ) {
            if (currentValue[selectAllLabel]) {
                currentValue = {};
                finalValues = [];
            } else {
                props.data.forEach((item) => {
                    currentValue[item.label] = true;
                });
                currentValue[selectAllLabel] = true;
                finalValues = [selectAllLabel];
            }
        } else if (isDefaultValue) {
            finalValues = value;
            currentValue = {};
            if (value) {
                value.forEach((item) => {
                    currentValue[item] = true;
                });
            }

            if (selectAllLabel && selectAllLabel in currentValue) {
                const { [selectAllLabel]: del, ...obj } = currentValue;
                currentValue = { ...obj };
            }
        } else {
            if (currentValue[value]) {
                const { [value]: del, ...rest } = currentValue;
                currentValue = { ...rest };
            } else {
                currentValue[value] = true;
            }

            if (selectAllLabel && selectAllLabel in currentValue) {
                const { [selectAllLabel]: del, ...obj } = currentValue;
                currentValue = { ...obj };
            }
            finalValues = Object.keys(currentValue);
        }

        const performUpdate = () => {
            this.setState(
                {
                    currentValue,
                },
                () => {
                    this.updateQuery(finalValues, props);
                    this.locked = false;
                    if (props.onValueChange) props.onValueChange(finalValues);
                },
            );
        };

        checkValueChange(props.componentId, finalValues, props.beforeValueChange, performUpdate);
    };

    updateQuery = (value, props) => {
        const query = props.customQuery || MultiDataList.defaultQuery;

        // find the corresponding value of the label for running the query
        const queryValue = value.reduce((acc, item) => {
            if (item === props.selectAllLabel) {
                return acc.concat(item);
            }
            const matchingItem = props.data.find(dataItem => dataItem.label === item);
            return matchingItem ? acc.concat(matchingItem.value) : acc;
        }, []);

        props.updateQuery({
            componentId: props.componentId,
            query: query(queryValue, props),
            value,
            label: props.filterLabel,
            showFilter: props.showFilter,
            URLParams: props.URLParams,
            componentType: 'MULTIDATALIST',
        });
    };

    static generateQueryOptions(props, state) {
        const queryOptions = getQueryOptions(props);
        const includes = state.options.map(item => item.value);
        return getAggsQuery(queryOptions, props, includes);
    }

    updateQueryOptions = (props) => {
        const queryOptions = MultiDataList.generateQueryOptions(props, this.state);
        props.setQueryOptions(this.internalComponent, queryOptions);
    };

    updateStateOptions = (bucket) => {
        if (bucket) {
            const bucketDictionary = bucket.reduce(
                (obj, item) => ({
                    ...obj,
                    [item.key]: item.doc_count,
                }),
                {},
            );

            const { options } = this.state;
            const newOptions = options.map((item) => {
                if (bucketDictionary[item.value]) {
                    return {
                        ...item,
                        count: bucketDictionary[item.value],
                    };
                }

                return item;
            });

            this.setState({
                options: newOptions,
            });
        }
    };

    handleInputChange = (e) => {
        const { value } = e.target;
        this.setState({
            searchTerm: value,
        });
    };

    renderSearch = () => {
        if (this.props.showSearch) {
            return (
                <Input
                    className={getClassName(this.props.innerClass, 'input') || null}
                    onChange={this.handleInputChange}
                    value={this.state.searchTerm}
                    placeholder={this.props.placeholder}
                    style={{
                        margin: '0 0 8px',
                    }}
                    themePreset={this.props.themePreset}
                />
            );
        }
        return null;
    };

    handleClick = (e) => {
        this.setValue(e.target.value);
    };

    render() {
        const { selectAllLabel, showCount, renderListItem } = this.props;
        const { options } = this.state;

        if (options.length === 0) {
            return null;
        }

        return (
            <Container style={this.props.style} className={this.props.className}>
                {this.props.title && (
                    <Title className={getClassName(this.props.innerClass, 'title') || null}>
                        {this.props.title}
                    </Title>
                )}
                {this.renderSearch()}
                <UL className={getClassName(this.props.innerClass, 'list') || null}>
                    {selectAllLabel ? (
                        <li
                            key={selectAllLabel}
                            className={`${this.state.currentValue[selectAllLabel] ? 'active' : ''}`}
                        >
                            <Checkbox
                                className={getClassName(this.props.innerClass, 'checkbox') || null}
                                id={`${this.props.componentId}-${selectAllLabel}`}
                                name={selectAllLabel}
                                value={selectAllLabel}
                                onChange={this.handleClick}
                                checked={!!this.state.currentValue[selectAllLabel]}
                                show={this.props.showCheckbox}
                            />
                            <label
                                className={getClassName(this.props.innerClass, 'label') || null}
                                htmlFor={`${this.props.componentId}-${selectAllLabel}`}
                            >
                                {selectAllLabel}
                            </label>
                        </li>
                    ) : null}
                    {options
                        .filter((item) => {
                            if (this.props.showSearch && this.state.searchTerm) {
                                return item.label
                                    .toLowerCase()
                                    .includes(this.state.searchTerm.toLowerCase());
                            }
                            return true;
                        })
                        .map(item => (
                            <li
                                key={item.label}
                                className={`${this.state.currentValue[item.label] ? 'active' : ''}`}
                            >
                                <Checkbox
                                    className={
                                        getClassName(this.props.innerClass, 'checkbox') || null
                                    }
                                    id={`${this.props.componentId}-${item.label}`}
                                    name={this.props.componentId}
                                    value={item.label}
                                    onChange={this.handleClick}
                                    checked={!!this.state.currentValue[item.label]}
                                    show={this.props.showCheckbox}
                                />
                                <label
                                    className={getClassName(this.props.innerClass, 'label') || null}
                                    htmlFor={`${this.props.componentId}-${item.label}`}
                                >
                                    {renderListItem ? (
                                        renderListItem(item.label, item.count)
                                    ) : (
                                        <span>
                                            {item.label}
                                            {showCount && item.count && (
                                                <span
                                                    className={
                                                        getClassName(
                                                            this.props.innerClass,
                                                            'count',
                                                        ) || null
                                                    }
                                                >
                                                    &nbsp;({item.count})
                                                </span>
                                            )}
                                        </span>
                                    )}
                                </label>
                            </li>
                        ))}
                </UL>
            </Container>
        );
    }
}

MultiDataList.propTypes = {
    addComponent: types.funcRequired,
    removeComponent: types.funcRequired,
    setQueryListener: types.funcRequired,
    setQueryOptions: types.funcRequired,
    updateQuery: types.funcRequired,
    watchComponent: types.funcRequired,
    selectedValue: types.selectedValue,
    options: types.options,
    // component props
    beforeValueChange: types.func,
    className: types.string,
    componentId: types.stringRequired,
    customQuery: types.func,
    data: types.data,
    dataField: types.stringRequired,
    defaultSelected: types.stringArray,
    filterLabel: types.string,
    innerClass: types.style,
    nestedField: types.string,
    onQueryChange: types.func,
    onValueChange: types.func,
    placeholder: types.string,
    queryFormat: types.queryFormatSearch,
    react: types.react,
    selectAllLabel: types.string,
    showCheckbox: types.boolRequired,
    showFilter: types.bool,
    showSearch: types.bool,
    style: types.style,
    themePreset: types.themePreset,
    title: types.title,
    URLParams: types.bool,
    showCount: types.bool,
    renderListItem: types.func,
};

MultiDataList.defaultProps = {
    className: null,
    placeholder: 'Search',
    queryFormat: 'or',
    showCheckbox: true,
    showFilter: true,
    showSearch: true,
    style: {},
    URLParams: false,
    showCount: false,
};

const mapStateToProps = (state, props) => ({
    selectedValue:
        (state.selectedValues[props.componentId]
            && state.selectedValues[props.componentId].value)
        || [],
    themePreset: state.config.themePreset,
    options:
        props.nestedField && state.aggregations[props.componentId]
            ? state.aggregations[props.componentId].reactivesearch_nested
            : state.aggregations[props.componentId],
});

const mapDispatchtoProps = dispatch => ({
    addComponent: component => dispatch(addComponent(component)),
    removeComponent: component => dispatch(removeComponent(component)),
    updateQuery: updateQueryObject => dispatch(updateQuery(updateQueryObject)),
    watchComponent: (component, react) => dispatch(watchComponent(component, react)),
    setQueryListener: (component, onQueryChange, beforeQueryChange) =>
        dispatch(setQueryListener(component, onQueryChange, beforeQueryChange)),
    setQueryOptions: (component, props) => dispatch(setQueryOptions(component, props)),
});

export default connect(
    mapStateToProps,
    mapDispatchtoProps,
)(MultiDataList);