toggle-corp/react-store

View on GitHub
components/Input/TabularSelectInput/index.js

Summary

Maintainability
A
0 mins
Test Coverage
import PropTypes from 'prop-types';
import React from 'react';
import { listToMap, isFalsy, _cs } from '@togglecorp/fujs';
import { FaramInputElement } from '@togglecorp/faram';

import DangerButton from '../../Action/Button/DangerButton';
import MultiSelectInput from '../MultiSelectInput';
import Table from '../../View/Table';

import HintAndError from '../HintAndError';

import styles from './styles.scss';

/**
 * comparator: comparator function for sorting, recieves data rows(not column data)
 *
 * defaultSortOrder: the sort order which should be applied when clicked,
 *
 * key: unique key for each column, the key is also used to determine
 *      the data for rows in the body
 *
 * label: text label for the column
 *
 * modifier: returns a renderable object for the column, recieves whole row of data (not column)
 *
 * order: the order in which they appear relative to that of other header columns
 *
 * sortable: is element sortable?
 */
const TableHeaderPropTypes = PropTypes.arrayOf(
    PropTypes.shape({
        comparator: PropTypes.func,
        defaultSortOrder: PropTypes.string,
        key: PropTypes.string,
        label: PropTypes.string,
        modifier: PropTypes.func,
        order: PropTypes.number,
        sortable: PropTypes.bool,
    }),
);

const propTypes = {
    blackList: PropTypes.oneOfType([
        PropTypes.string,
        PropTypes.number,
        PropTypes.arrayOf(
            PropTypes.string,
        ),
        PropTypes.arrayOf(
            PropTypes.number,
        ),
    ]),

    /**
     * Key selector function
     * should return key from provided row data
     */
    keySelector: PropTypes.func,

    onChange: PropTypes.func,

    value: PropTypes.arrayOf(
        PropTypes.object,
    ),

    /**
     * String to show in case of error
     */
    error: PropTypes.string,

    /**
     * Hint text
     */
    hint: PropTypes.string,

    showHintAndError: PropTypes.bool,

    disabled: PropTypes.bool,
    readOnly: PropTypes.bool,

    /**
     * Value selector function
     * should return value from provided row data
     */
    labelSelector: PropTypes.func,

    hideRemoveFromListButton: PropTypes.bool,

    className: PropTypes.string,
    /**
     * Options to be shown
     */
    options: PropTypes.arrayOf(
        PropTypes.object,
    ),

    /**
     * headers is an array of the structure objects required for the header
     *
     * NOTE: see { TableHeaderPropTypes } in Table/Header for more detail
     */
    tableHeaders: TableHeaderPropTypes,
};

const defaultProps = {
    className: '',
    keySelector: d => (d || {}).key,
    labelSelector: d => (d || {}).label,
    onChange: undefined,
    hideRemoveFromListButton: false,
    error: '',
    hint: '',
    options: [],
    blackList: [],
    value: [],
    disabled: false,
    readOnly: false,
    showHintAndError: true,
    tableHeaders: [],
};

class TabularSelectInput extends React.PureComponent {
    static propTypes = propTypes;

    static defaultProps = defaultProps;

    constructor(props) {
        super(props);

        const {
            options,
            blackList,
            keySelector,
            tableHeaders,
            value,
        } = this.props;

        const selectedOptions = this.getSelectedOptions(value, keySelector, blackList, options);
        const tableHeadersWithRemove = this.createTableHeaders(tableHeaders);
        const validOptions = this.getValidOptions(options, keySelector, blackList);
        const selectedOptionsKeys = selectedOptions.map(d => keySelector(d));

        this.state = {
            validOptions,
            tableHeadersWithRemove,
            selectedOptions,
            selectedOptionsKeys,
        };
    }

    // eslint-disable-next-line camelcase
    UNSAFE_componentWillReceiveProps(nextProps) {
        const {
            tableHeaders,
            blackList,
            options,
            value,
        } = this.props;

        if (nextProps.tableHeaders !== tableHeaders) {
            const tableHeadersWithRemove = this.createTableHeaders(nextProps.tableHeaders);
            this.setState({ tableHeadersWithRemove });
        }

        if (
            nextProps.blackList !== blackList
            || nextProps.options !== options
        ) {
            const validOptions = this.getValidOptions(
                nextProps.options,
                nextProps.keySelector,
                nextProps.blackList,
            );
            this.setState({
                validOptions,
            });
        }

        if (nextProps.value !== value) {
            const selectedOptions = this.getSelectedOptions(
                nextProps.value,
                nextProps.keySelector,
                blackList,
                nextProps.options,
            );
            const selectedOptionsKeys = selectedOptions.map(d => nextProps.keySelector(d));

            this.setState({
                selectedOptions,
                selectedOptionsKeys,
            });
        }
    }

    getClassName = () => {
        const { className } = this.props;
        const { error } = this.state;

        const classNames = [
            className,
            styles.tabularSelectInput,
            'tabular-select-input',
        ];

        if (!isFalsy(error, [''])) {
            classNames.push(styles.error);
            classNames.push('error');
        }

        return classNames.join(' ');
    }

    getSelectedOptions = (values, keySelector, blackList, options) => {
        const blackListMap = listToMap(
            blackList,
            d => d,
            () => true,
        );
        const optionsMap = listToMap(
            options,
            d => keySelector(d),
        );

        const selectedOptions = values
            .map(v => ({
                ...optionsMap[keySelector(v)],
                ...v,
            }))
            .filter(
                value => !blackListMap[keySelector(value)],
            );

        return selectedOptions;
    }

    getValidOptions = (options, keySelector, blackList) => {
        const blackListMap = listToMap(
            blackList,
            d => d,
            () => true,
        );

        const validOptions = options.filter(
            option => !blackListMap[keySelector(option)],
        );
        return validOptions;
    }

    createTableHeaders = (tableHeaders) => {
        const {
            hideRemoveFromListButton,
        } = this.props;

        if (hideRemoveFromListButton) {
            return tableHeaders;
        }

        return ([
            ...tableHeaders,
            {
                key: 'delete-action-included',
                label: 'Remove',
                modifier: row => (
                    <DangerButton
                        className="delete-button"
                        onClick={() => this.handleRemoveButtonClick(row)}
                        iconName="delete"
                        smallVerticalPadding
                        transparent
                        disabled={this.props.disabled || this.props.readOnly}
                    />
                ),
            },
        ]);
    }

    handleSelectInputChange = (values) => {
        const {
            keySelector,
            onChange,
        } = this.props;
        const {
            validOptions,
        } = this.state;

        const selectedOptions = [];
        values.forEach((v) => {
            const rowIndex = validOptions.findIndex(u => keySelector(u) === v);
            if (rowIndex !== -1) {
                selectedOptions.push(validOptions[rowIndex]);
            }
        });
        const selectedOptionsKeys = selectedOptions.map(d => keySelector(d));

        this.setState(
            {
                selectedOptions,
                selectedOptionsKeys,
            },
            () => {
                if (onChange) {
                    onChange(selectedOptions);
                }
            },
        );
    }

    handleRemoveButtonClick = (row) => {
        const {
            keySelector,
            onChange,
        } = this.props;
        const { selectedOptions } = this.state;

        // Remove from selectedOptions
        const removedElementKey = keySelector(row);
        const index = selectedOptions.findIndex(
            d => keySelector(d) === removedElementKey,
        );
        const selectedOptionsNew = [...selectedOptions];
        selectedOptionsNew.splice(index, 1);

        const selectedOptionsKeys = selectedOptionsNew.map(d => keySelector(d));

        this.setState(
            {
                selectedOptions: selectedOptionsNew,
                selectedOptionsKeys,
            },
            () => {
                if (onChange) {
                    onChange(selectedOptionsNew);
                }
            },
        );
    }

    render() {
        const {
            keySelector,
            labelSelector,
            error,
            hint,
            showHintAndError,
            disabled,
            onChange, // eslint-disable-line no-unused-vars, @typescript-eslint/no-unused-vars
            value, // eslint-disable-line no-unused-vars, @typescript-eslint/no-unused-vars
            options, // eslint-disable-line no-unused-vars, @typescript-eslint/no-unused-vars
            className, // eslint-disable-line no-unused-vars, @typescript-eslint/no-unused-vars
            ...otherProps
        } = this.props;

        const {
            selectedOptions,
            selectedOptionsKeys,
            validOptions,
            tableHeadersWithRemove,
        } = this.state;

        return (
            <div className={this.getClassName()}>
                <MultiSelectInput
                    className={styles.select}
                    value={selectedOptionsKeys}
                    options={validOptions}
                    keySelector={keySelector}
                    labelSelector={labelSelector}
                    onChange={this.handleSelectInputChange}
                    disabled={disabled}
                    error={error}
                    showHintAndError={false}
                    {...otherProps}
                />
                <div className={_cs(styles.tableContainer, 'table-container')}>
                    <Table
                        data={selectedOptions}
                        headers={tableHeadersWithRemove}
                        keySelector={keySelector}
                    />
                </div>
                <HintAndError
                    show={showHintAndError}
                    hint={hint}
                    error={error}
                />
            </div>
        );
    }
}

export default FaramInputElement(TabularSelectInput);