Source

Utils/Menus/AttributesMenu/AttributesMenu.js

import React from "react";
import PropTypes from "prop-types";
import { withStyles } from "@material-ui/core/styles";
import { fetchDescriptiveAttributes, fetchObjectNames } from "../../utilFunctions/fetchFunctions";
import { CustomControlLabel, CustomFormLabel } from "../../Inputs";
import StyledRadioButton from "../../Inputs/StyledRadioButton";
import Menu from "@material-ui/core/Menu";
import RadioGroup from "@material-ui/core/RadioGroup";
import Zoom from "@material-ui/core/Zoom";
import { AutoSizer, List } from "react-virtualized";
import Check from "@material-ui/icons/Check";
import {MuiMenuPropTypes} from "../MuiMenu";


const CustomMenu = withStyles( theme => ({
    paper: {
        backgroundColor: theme.palette.background.main1,
        color: theme.palette.text.main1
    }
}), {name: "AttributesMenu"})(props => <Menu {...props} />);

/**
 * <h3>Overview</h3>
 * Allows a user to choose object's visible name. When mounted makes an API call
 * to retrieve available names from server.
 * When a user changes name, makes an API call to save selection.
 *
 * @constructor
 * @category Menus
 * @param {Object} props
 * @param {Object} props.ListProps - Props applied to the List element from react-virtualized.
 * @param {string} props.MuiMenuProps - Props applied to the Menu element from Material-UI.
 * @param {string} props.objectGlobalName - The global visible object name used by all tabs as reference.
 * @param {function} props.onAttributesRefreshed - Callback fired when component has just refreshed it's content.
 * @param {function} props.onObjectNamesChange - Callback fired when object names have been changed.
 * @param {function} props.onSnackbarOpen - Callback fired when the component requests to display an error.
 * @param {string} props.projectId - The identifier of a selected project.
 * @param {boolean} props.refreshNeeded - If <code>true</code> the component will refresh it's content.
 * @param {string} props.resource - The name of a selected resource.
 * @param {string} props.serverBase - The host in the URL of an API call.
 * @param {Object} props.queryParams - The query parameters in the URL of an API call.
 * @param {number} props.queryParams.subject - The index of a subject that contains object names.
 * @param {string} props.queryParams.set - The name of the set that narrows down object names.
 * @returns {React.PureComponent}
 */
class AttributesMenu extends React.PureComponent {
    constructor(props) {
        super(props);

        this.state = {
            loading: {
                attributes: false,
                objectVisibleName: false,
                objectNames: false
            },
            attributes: ["Default"],
            objectVisibleName: "Default",
            paddingRight: 0,
        }

        this._attributes = ["Default"];
    }

    componentDidMount() {
        this._isMounted = true;

        this.getAttributes();
    }

    componentDidUpdate(prevProps, prevState, snapshot) {
        if (prevProps.projectId !== this.props.projectId) {
            this.getAttributes();
            return;
        }

        if (prevProps.MuiMenuProps.anchorEl == null && this.props.MuiMenuProps.anchorEl != null
            && this.props.refreshNeeded) {

            this.getAttributes(() => this.getObjectNames(this.props.onAttributesRefreshed));
            return;
        }

        if (prevProps.objectGlobalName !== this.props.objectGlobalName) {
            if (prevProps.objectGlobalName != null && this.props.objectGlobalName == null) {
                return;
            }

            this.getAttributes(this.getObjectNames);
        }
    }

    componentWillUnmount() {
        this._isMounted = false;
    }

    defaultRowRenderer = ({key, index, style}) => {
        const { attributes } = this.state;

        return (
            <CustomControlLabel
                control={
                    <StyledRadioButton
                        checkedIcon={<Check />}
                        icon={<span style={{ display: "none" }} />}
                        inputProps={{ style: { display: "none" }}}
                        style={{ padding: 0 }}
                    />
                }
                key={key}
                label={attributes[index]}
                style={style}
                value={attributes[index]}
            />
        )
    }

    processDescriptiveAttributes = (result) => {
        if (this._isMounted && result != null
            && result.hasOwnProperty("available") && result.hasOwnProperty("actual")) {

            this.setState({
                attributes: [...this._attributes, ...result.available],
                objectVisibleName: result.actual === null ? "Default" : result.actual
            });
        }
    }

    getAttributes = (finallyCallback) => {
        this.setState(({loading}) => ({
            loading: { ...loading, attributes: true }
        }), () => {
            const { projectId, resource, serverBase } = this.props;
            const pathParams = { projectId };
            const queryParams = { objectVisibleName: undefined };
            const method = "GET";

            fetchDescriptiveAttributes(
                resource, pathParams, queryParams, method, serverBase
            ).then(result => {
                this.processDescriptiveAttributes(result);
            }).catch(exception => {
                this.props.onSnackbarOpen(exception);
            }).finally(() => {
                if (this._isMounted) {
                    this.setState(({loading}) => ({
                        loading: { ...loading, attributes: false }
                    }), () => {
                        if (typeof finallyCallback === "function") finallyCallback();
                    });
                }
            });
        });
    }

    saveSelection = (event) => {
        const objectVisibleName = event.target.value === "Default" ? undefined : event.target.value;

        this.setState(({loading}) => ({
            loading: { ...loading, objectVisibleName: true }
        }), () => {
            const { projectId, resource, serverBase } = this.props;
            const pathParams = { projectId };
            const queryParams = { objectVisibleName };
            const method = "POST";

            fetchDescriptiveAttributes(
                resource, pathParams, queryParams, method, serverBase
            ).then(result => {
                this.processDescriptiveAttributes(result);
            }).catch(exception => {
                this.props.onSnackbarOpen(exception);
            }).finally(() => {
                if (this._isMounted) {
                    this.setState(({loading}) => ({
                        loading: { ...loading, objectVisibleName: false }
                    }), () => this.getObjectNames());
                }
            });
        });
    }

    getObjectNames = (finallyCallback) => {
        this.setState(({loading}) => ({
            loading: { ...loading, objectNames: true}
        }), () => {
            const { projectId, resource, serverBase, queryParams } = this.props;
            const pathParams = { projectId };

            const onFinallyCallback = () => {
                this.setState(({loading}) => ({
                    loading: { ...loading, objectNames: false }
                }), () => {
                    if (typeof finallyCallback === "function") finallyCallback();
                });
            }

            if (this._isMounted && queryParams != null
                && queryParams.subject != null && queryParams.subject < 0) {

                onFinallyCallback();
                return;
            }

            fetchObjectNames(
                resource, pathParams, queryParams, serverBase
            ).then(result => {
                if (this._isMounted && Array.isArray(result)) {
                    this.props.onObjectNamesChange(result);
                }
            }).catch(
                this.props.onSnackbarOpen
            ).finally(
                onFinallyCallback
            );
        });
    }

    updatePaddingRight = () => {
        const { ListProps: { id }} = this.props;
        const scrollHeight = document.getElementById(id).scrollHeight;
        const clientHeight = document.getElementById(id).clientHeight;

        this.setState({
            paddingRight: scrollHeight > clientHeight ? 0 : 8
        });
    }

    render() {
        const { attributes, objectVisibleName, paddingRight } = this.state;
        const { ListProps, MuiMenuProps } = this.props;

        const rowHeight = ListProps.hasOwnProperty("rowHeight") ? ListProps.rowHeight : 28;

        return (
            <CustomMenu
                anchorEl={MuiMenuProps.anchorEl}
                anchorOrigin={{
                    vertical: 'bottom',
                    horizontal: 'center'
                }}
                getContentAnchorEl={null}
                keepMounted={true}
                onEntering={this.updatePaddingRight}
                open={Boolean(MuiMenuProps.anchorEl)}
                MenuListProps={{
                    style: {
                        paddingLeft: 16,
                        paddingRight: paddingRight,
                        width: "auto"
                    }
                }}
                transformOrigin={{
                    vertical: "top",
                    horizontal: "left"
                }}
                TransitionComponent={Zoom}
                {...MuiMenuProps}
            >
                <CustomFormLabel style={{outline: 0}}>
                    Choose visible object name:
                </CustomFormLabel>
                <RadioGroup
                    onChange={this.saveSelection}
                    value={objectVisibleName}
                    style={{
                        height: rowHeight * attributes.length,
                        maxHeight: rowHeight * 4,
                        width: "15rem"
                    }}
                >
                    <AutoSizer>
                        {({height, width}) => (
                            <List
                                height={height}
                                rowCount={attributes.length}
                                rowHeight={28}
                                rowRenderer={this.defaultRowRenderer}
                                style={{outline: 0}}
                                width={width}
                                {...ListProps}
                            />
                        )}
                    </AutoSizer>
                </RadioGroup>
            </CustomMenu>
        );
    }
}

AttributesMenu.propTypes = {
    ListProps: PropTypes.shape({
        id: PropTypes.string.isRequired,
    }),
    MuiMenuProps: PropTypes.shape({ ...MuiMenuPropTypes }),
    objectGlobalName: PropTypes.string,
    onAttributesRefreshed: PropTypes.func,
    onObjectNamesChange: PropTypes.func,
    onSnackbarOpen: PropTypes.func,
    projectId: PropTypes.string.isRequired,
    refreshNeeded: PropTypes.bool,
    resource: PropTypes.string.isRequired,
    serverBase: PropTypes.string,
    queryParams: PropTypes.shape({
        subject: PropTypes.number,
        set: PropTypes.string
    })
}

AttributesMenu.defaultProps = {
    refreshNeeded: false
}

export default AttributesMenu;