Source

Utils/Dialogs/DetailsDialog/ClassifiedObjectDialog.js

import React from "react";
import PropTypes from "prop-types";
import { getTitleConditions, getTitleDecisions } from "./Utils";
import { fetchObject, fetchRule } from "../../utilFunctions/fetchFunctions";
import { getItemName, getRuleName } from "../../utilFunctions/parseItems";
import { MultiColumns } from "../../Containers";
import ColouredTitle from "../../DataDisplay/ColouredTitle";
import CustomTooltip from "../../DataDisplay/CustomTooltip";
import { FullscreenDialog, FullscreenHeader } from "../FullscreenDialog";
import ObjectTable from "../../DataDisplay/ObjectTable";
import RuleTable, { estimateTableHeight } from "../../DataDisplay/RuleTable";
import TableItemsList from "../../DataDisplay/TableItemsList";
import TraitsTable from "../../DataDisplay/TraitsTable";
import StyledCircularProgress from "../../Feedback/StyledCircularProgress";
import { AttributesMenu } from "../../Menus/AttributesMenu";
import { Slides } from "../../Navigation/Slides";
import CustomHeader from "../../Surfaces/CustomHeader";
import ArrowBack from "@material-ui/icons/ArrowBack";

/**
 * <h3>Overview</h3>
 * The fullscreen dialog with details of a selected classified object.
 *
 * @constructor
 * @category Dialogs
 * @subcategory Details Dialogs
 * @param {Object} props
 * @param {string} props.coveredObjectResource - The name of a selected resource when fetching covered object.
 * @param {boolean} props.disableAttributesMenu - If <code>true</code> the attributes menu will be disabled.
 * @param {Object} props.item - The selected object with it's characteristics.
 * @param {number} props.item.id - The id of a selected object.
 * @param {Object} props.item.name - The name of a selected object.
 * @param {number|string} props.item.name.primary - The part of a name coloured with a primary colour.
 * @param {number|string} props.item.name.secondary - The part of a name coloured with a secondary colour.
 * @param {function} props.item.name.toString - Returns name as a single string.
 * @param {Object} props.item.traits - The characteristics of a selected object in a key-value form.
 * @param {string|number} props.item.traits.originalDecision - The original classification.
 * @param {string|number} props.item.traits.suggestedDecision - The suggested classification.
 * @param {number} props.item.traits.certainty - The certainty of suggested classification.
 * @param {number} props.item.traits.indicesOfCoveringRules - The number of rules that cover a selected object.
 * @param {function} props.item.toFilter - Returns item in an easy to filter form.
 * @param {string} props.objectGlobalName - The global visible object name used by all tabs as reference.
 * @param {function} props.onClose - Callback fired when the component requests to be closed.
 * @param {function} props.onSnackbarOpen - Callback fired when the component requests to display an error.
 * @param {boolean} props.open - If <code>true</code> the Dialog is open.
 * @param {string} props.projectId - The identifier of a selected project.
 * @param {string} props.resource - The name of a selected resource when fetching.
 * @param {string} props.serverBase - The host and port in the URL of an API call.
 * @returns {React.PureComponent}
 */
class ClassifiedObjectDialog extends React.PureComponent {
    constructor(props) {
        super(props);

        this.state = {
            loading: {
                object: false,
                rule: false,
                coveredObjects: false,
                coveredObject: false,
            },
            requestIndex: {
                object: false,
                rule: false,
                coveredObjects: false,
                coveredObject: false
            },
            object: null,
            coveringRules: [],
            attributes: [],
            rule: null,
            ruleTraits: null,
            ruleIndex: -1,
            coveredObjects: [],
            coveredSupportingObjects: [],
            coveredObjectNames: [],
            coveredAttributes: [],
            coveredObject: null,
            coveredObjectIndex: -1,
            ruleTableHeight: 0,
            direction: "forward",
            slide: 0,
            sliding: false,
            attributesMenuEl: null,
        };
    }

    componentDidMount() {
        this._isMounted = true;

        const { item: { id }} = this.props;
        this.getObject(id, true);
    }

    componentDidUpdate(prevProps, prevState, snapshot) {
        if (prevProps.projectId !== this.props.projectId) {
            this.setState({
                object: null,
                coveringRules: [],
                attributes: [],
                rule: null,
                ruleTraits: null,
                ruleIndex: -1,
                coveredObjects: [],
                coveredSupportingObjects: [],
                coveredObjectNames: [],
                coveredAttributes: [],
                coveredObject: null,
                coveredObjectIndex: - 1,
                direction: "forward",
                slide: 0
            });

            return;
        }

        if (prevProps.item.id !== this.props.item.id) {
            this.setState({
                object: null,
                coveringRules: [],
                rule: null,
                ruleTraits: null,
                ruleIndex: -1,
                coveredObjects: [],
                coveredSupportingObjects: [],
                coveredObjectNames: [],
                coveredObject: null,
                coveredObjectIndex: -1,
                direction: "forward",
                slide: 0
            }, () => {
                const { item: { id }} = this.props;
                this.getObject(id, false);
            });

            return;
        }

        if (prevState.ruleIndex !== this.state.ruleIndex) {
            this.setState({
                coveredObjects: [],
                coveredSupportingObjects: [],
                coveredObjectNames: [],
                coveredObject: null,
                coveredObjectIndex: -1
            }, () => {
                if (this.state.ruleIndex > -1) this.getCoveredObjects(this.state.ruleIndex)
            });
        }

        if (this.state.ruleIndex > -1) {
            const height = estimateTableHeight(this.state.rule);

            if (prevState.ruleTableHeight !== height) {
                this.setState({
                    ruleTableHeight: height
                });
            }
        }
    }

    componentWillUnmount() {
        this._isMounted = false;
    }

    getObject = (objectIndex, isAttributes) => {
        let localRequestIndex = 0;

        this.setState(({loading, requestIndex}) => {
            localRequestIndex = requestIndex.object;

            return {
                loading: { ...loading, object: true },
                requestIndex: { ...requestIndex, object: localRequestIndex + 1 }
            };
        }, () => {
            const { projectId, resource, serverBase } = this.props;
            const pathParams = { projectId };
            const queryParams = { objectIndex, isAttributes };

            fetchObject(
                resource, pathParams, queryParams, serverBase
            ).then(result => {
                if (this._isMounted && result != null && result.hasOwnProperty("object")
                    && result.hasOwnProperty("indicesOfCoveringRules")) {

                    this.setState(({requestIndex, attributes}) => {
                        if (requestIndex.object !== localRequestIndex + 1) {
                            return { };
                        }

                        return {
                            requestIndex: { ...requestIndex, object: 0 },
                            object: result.object,
                            coveringRules: result.indicesOfCoveringRules,
                            attributes: result.hasOwnProperty("attributes") ? result.attributes : attributes
                        };
                    });
                }
            }).catch(exception => {
                this.props.onSnackbarOpen(exception);
            }).finally(() => {
                if (this._isMounted) {
                    this.setState(({loading}) => ({
                        loading: { ...loading, object: false },
                    }));
                }
            });
        });
    }

    getCoveringRule = (ruleIndex, finallyCallback) => {
        let localRequestIndex = 0;

        this.setState(({loading, requestIndex}) => {
            localRequestIndex = requestIndex.rule;

            return {
                loading: { ...loading, rule: true },
                requestIndex: { ...requestIndex, rule: localRequestIndex + 1 }
            };
        }, () => {
            const { projectId, resource: resourceBase, serverBase } = this.props;
            const resource = resourceBase + "/rules";
            const pathParams = { projectId, ruleIndex };

            fetchRule(
                resource, pathParams, serverBase
            ).then(result => {
                if (this._isMounted && result != null && result.hasOwnProperty("rule")
                    && result.hasOwnProperty("ruleCharacteristics")) {

                    this.setState(({requestIndex}) => {
                        if (requestIndex.rule !== localRequestIndex + 1) {
                            return { };
                        }

                        return {
                            requestIndex: { ...requestIndex, rule: 0 },
                            rule: result.rule,
                            ruleTraits: result.ruleCharacteristics
                        };
                    });
                }
            }).catch(exception => {
                this.props.onSnackbarOpen(exception);
            }).finally(() => {
                if (this._isMounted) {
                    this.setState(({loading}) => ({
                        loading: { ...loading, rule: false }
                    }), () => {
                        if (typeof finallyCallback === "function") finallyCallback();
                    });
                }
            });
        });
    };

    getCoveredObjects = (ruleIndex) => {
        let localRequestIndex = 0;

        this.setState(({loading, requestIndex}) => {
            localRequestIndex = requestIndex.coveredObjects;

            return {
                loading: { ...loading, coveredObjects: true },
                requestIndex: { ...requestIndex, coveredObjects: localRequestIndex + 1 }
            };
        }, () => {
            const { projectId, resource: resourceBase, serverBase } = this.props;
            const resource = resourceBase + "/rules";
            const pathParams = { projectId, ruleIndex };

            fetchRule(
                resource, pathParams, serverBase, true
            ).then(result => {
                if (this._isMounted && result != null && result.hasOwnProperty("objectNames")
                    && result.hasOwnProperty("indicesOfCoveredObjects") && result.hasOwnProperty("isSupportingObject")) {

                    this.setState(({requestIndex}) => {
                        if (requestIndex.coveredObjects !== localRequestIndex + 1) {
                            return { };
                        }

                        return {
                            requestIndex: { ...requestIndex, coveredObjects: 0 },
                            coveredObjects: result.indicesOfCoveredObjects,
                            coveredSupportingObjects: result.isSupportingObject,
                            coveredObjectNames: result.objectNames
                        };
                    });
                }
            }).catch(exception => {
                this.props.onSnackbarOpen(exception);
            }).finally(() => {
                if (this._isMounted) {
                    this.setState(({loading}) => ({
                        loading: { ...loading, coveredObjects: false }
                    }));
                }
            });
        });
    };

    getCoveredObject = (objectIndex, finallyCallback) => {
        let localRequestIndex = 0;

        this.setState(({loading, requestIndex}) => {
            localRequestIndex = requestIndex.coveredObject;

            return {
                loading: { ...loading, coveredObject: true },
                requestIndex: { ...requestIndex, coveredObject: localRequestIndex + 1 }
            };
        }, () => {
            const { coveredAttributes } = this.state;
            const { coveredObjectResource, projectId, resource: resourceBase, serverBase } = this.props;

            const resource = coveredObjectResource != null ? coveredObjectResource : resourceBase + "/rules";
            const pathParams = { projectId };
            const queryParams = { objectIndex, isAttributes: coveredAttributes.length === 0 };

            fetchObject(
                resource, pathParams, queryParams, serverBase
            ).then(result => {
                if (this._isMounted && result != null && result.hasOwnProperty("value")) {
                    this.setState(({requestIndex, coveredAttributes}) => {
                        if (requestIndex.coveredObject !== localRequestIndex + 1) {
                            return { };
                        }

                        return {
                            requestIndex: { ...requestIndex, coveredObject: 0 },
                            coveredObject: result.value,
                            coveredAttributes: result.hasOwnProperty("attributes") ?
                                result.attributes : coveredAttributes
                        };
                    });
                }
            }).catch(exception => {
                this.props.onSnackbarOpen(exception);
            }).finally(() => {
                if (this._isMounted) {
                    this.setState(({loading}) => ({
                        loading: { ...loading, coveredObject: false }
                    }), () => {
                        if (typeof finallyCallback === "function") finallyCallback();
                    })
                }
            })
        })
    }

    onEnter = () => {
        this.setState({
            direction: "forward",
            slide: 0
        });
    };

    onEscapeKeyDown = () => {
        const { slide } = this.state;

        if (slide === 0) {
            this.props.onClose();
        } else {
            this.slide("backward", 0);
        }
    };

    onItemInTableSelected = (index) => {
        const finallyCallback = () => this.setState({ coveredObjectIndex: index });
        this.getCoveredObject(index, finallyCallback)
    };

    onCoveringRuleSelected = (index) => {
        const finallyCallback = () => this.setState({ ruleIndex: index });
        this.getCoveringRule(index, finallyCallback);
    };

    onCoveredObjectNamesChange = (names) => {
        this.setState({
            coveredObjectNames: names
        });
    };

    onAttributesMenuOpen = (event) => {
        const currentTarget = event.currentTarget;

        this.setState({
            attributesMenuEl: currentTarget
        });
    };

    onAttributesMenuClose = () => {
        this.setState({
            attributesMenuEl: null
        })
    }

    getClassificationTitle = () => {
        const { item } = this.props;

        return (
            <ColouredTitle
                text={[
                    { primary: "Selected object:"},
                    { ...item.name, brackets: false, }
                ]}
            />
        );
    };

    getRulesTitle = () => {
        const { ruleIndex, rule } = this.state;

        if (ruleIndex > -1) {
            const ruleName = getRuleName(rule);

            return (
                <ColouredTitle
                    text={[
                        { primary: `Rule ${ruleIndex + 1}: ` },
                        ...getTitleDecisions(ruleName.decisions),
                        { secondary: "\u2190" },
                        ...getTitleConditions(ruleName.conditions)
                    ]}
                />
            );
        } else {
            return (
                <div style={{display: "none"}}/>
            );
        }
    };

    getCoveredObjectName = (index) => {
        const { coveredObjects, coveredObjectNames } = this.state;
        return getItemName(coveredObjects.indexOf(index), coveredObjectNames).toString();
    };

    getCoveredObjectStyle = (index) => {
        const { ruleIndex, coveredObjects, coveredSupportingObjects } = this.state;

        if (ruleIndex > -1) {
            if (coveredSupportingObjects[coveredObjects.indexOf(index)]) {
                return { borderLeft: "4px solid green" };
            } else {
                return { borderLeft: "4px solid red" };
            }
        } else {
            return { };
        }
    };

    getSlotStyle = (index) => {
        if (index === 1) {
            return { width: "100%" };
        } else {
            return {};
        }
    };

    getHeaderStyle = (index) => {
        const { sliding } = this.state;
        let style = { display: "flex", flex: "1 0 100%" };

        if (sliding && index === 1) {
            style = { ...style, overflowX: "hidden" };
        }

        return style;
    };

    slide = (direction, nextSlide) => {
        this.setState({
            direction: direction,
            sliding: true
        }, () => {
            setTimeout(() => this.setState({
                direction: "forward",
                slide: nextSlide,
                sliding: false
            }), 1000);
        });
    }

    render() {
        const {
            loading,
            object,
            coveringRules,
            attributes,
            rule,
            ruleTraits,
            ruleIndex,
            coveredObjects,
            coveredObject,
            coveredObjectIndex,
            coveredAttributes,
            direction,
            ruleTableHeight,
            slide,
            sliding,
            attributesMenuEl,
        } = this.state;

        const {
            disableAttributesMenu,
            item,
            objectGlobalName,
            projectId,
            resource,
            serverBase,
            open
        } = this.props;

        const { originalDecision, suggestedDecision, certainty } = item.traits;

        return (
            <FullscreenDialog
                disableEscapeKeyDown={true}
                keepMounted={true}
                onClose={this.props.onClose}
                onEnter={this.onEnter}
                onEscapeKeyDown={this.onEscapeKeyDown}
                open={open}
            >
                <CustomHeader style={{ padding: 0 }}>
                    <Slides
                        direction={direction}
                        getSlotStyle={this.getSlotStyle}
                        sliding={sliding}
                        value={slide}
                    >
                        <FullscreenHeader
                            HeaderComponent={"div"}
                            HeaderProps={{ style: { ...this.getHeaderStyle(0) }}}
                            id={"classified-details-header"}
                            onClose={this.props.onClose}
                            optional={
                                <React.Fragment>
                                    <span aria-label={"original-decision"}>
                                        {`Original decision: ${originalDecision}`}
                                    </span>
                                    <span aria-label={"suggested-decision"}>
                                        {`Certainty: ${certainty}   |   Suggested decision: ${suggestedDecision}`}
                                    </span>
                                </React.Fragment>
                            }
                            title={this.getClassificationTitle()}
                        />
                        <FullscreenHeader
                            closeIcon={<ArrowBack />}
                            CloseButtonProps={{
                                onMouseEnter: undefined,
                                onMouseLeave: undefined
                            }}
                            CloseTooltipProps={{ title: "Go back" }}
                            HeaderComponent={"div"}
                            HeaderProps={{ style: { ...this.getHeaderStyle(1) }}}
                            id={"classified-rules-header"}
                            onClose={() => this.slide("backward", 0)}
                            title={this.getRulesTitle()}
                        />
                    </Slides>
                </CustomHeader>
                <Slides
                    direction={direction}
                    sliding={sliding}
                    value={slide}
                >
                    <MultiColumns>
                        <div id={"classified-object"} style={{display: "flex", flexDirection: "column", width: "40%"}}>
                            {loading.object &&
                                <StyledCircularProgress />
                            }
                            {!loading.object && object != null &&
                                <ObjectTable
                                    attributes={attributes}
                                    object={object}
                                    objectHeader={item.name.toString()}
                                />
                            }
                        </div>
                        <div id={"classified-covering-rules"} style={{display: "flex", flexDirection: "column", width: "15%"}}>
                            {loading.object &&
                                <StyledCircularProgress/>
                            }
                            {!loading.object && object != null &&
                                <TableItemsList
                                    customisable={false}
                                    headerText={"Covering rules"}
                                    itemIndex={ruleIndex}
                                    itemText={"Rule"}
                                    onItemInTableSelected={this.onCoveringRuleSelected}
                                    table={coveringRules}
                                />
                            }
                        </div>
                        <div id={"classified-rules-traits"} style={{display: "flex", flexDirection: "column", width: "40%"}}>
                            {loading.rule &&
                                <StyledCircularProgress />
                            }
                            {!loading.rule && ruleIndex > -1 &&
                                <React.Fragment>
                                    <CustomTooltip
                                        arrow={true}
                                        enterDelay={250}
                                        enterNextDelay={500}
                                        placement={"top"}
                                        title={"Double click to see details"}
                                        WrapperProps={{
                                            id: "rule-table",
                                            onDoubleClick: () => this.slide("forward", 1),
                                            style: { marginBottom: "5%", minHeight: ruleTableHeight }
                                        }}
                                    >
                                        <RuleTable rule={rule} />
                                    </CustomTooltip>
                                    <div id={"traits-table"} style={{flexGrow: 1}}>
                                        <TraitsTable traits={ruleTraits} />
                                    </div>
                                </React.Fragment>
                            }
                        </div>
                    </MultiColumns>
                    <MultiColumns>
                        <div id={"rule-traits"} style={{display: "flex", flexDirection: "column", width: "20%"}}>
                            {loading.rule &&
                                <StyledCircularProgress />
                            }
                            {!loading.rule && ruleIndex > -1 &&
                                <TraitsTable traits={ruleTraits}/>
                            }
                        </div>
                        <div id={"rule-covered-object-list"} style={{display: "flex", flexDirection: "column", width: "20%"}}>
                            {loading.coveredObjects &&
                                <StyledCircularProgress />
                            }
                            {!loading.coveredObjects && Array.isArray(coveredObjects) && coveredObjects.length > 0 &&
                                <TableItemsList
                                    customisable={!disableAttributesMenu}
                                    getItemsStyle={this.getCoveredObjectStyle}
                                    getName={this.getCoveredObjectName}
                                    headerText={"Covered objects"}
                                    itemIndex={coveredObjectIndex}
                                    onItemInTableSelected={this.onItemInTableSelected}
                                    onSettingsClick={this.onAttributesMenuOpen}
                                    table={coveredObjects}
                                />
                            }
                        </div>
                        <div id={"rule-covered-object-details"} style={{display: "flex", flexDirection: "column", width: "40%"}}>
                            {loading.coveredObject &&
                                <StyledCircularProgress />
                            }
                            {!loading.coveredObject && coveredObjectIndex > -1 &&
                                <ObjectTable
                                    attributes={coveredAttributes}
                                    object={coveredObject}
                                    objectHeader={this.getCoveredObjectName(coveredObjectIndex).toString()}
                                />
                            }
                        </div>
                    </MultiColumns>
                </Slides>
                {!disableAttributesMenu &&
                    <AttributesMenu
                        ListProps={{
                            id: "classified-object-desc-attributes-menu"
                        }}
                        MuiMenuProps={{
                            anchorEl: attributesMenuEl,
                            onClose: this.onAttributesMenuClose
                        }}
                        objectGlobalName={objectGlobalName}
                        onObjectNamesChange={this.onCoveredObjectNamesChange}
                        onSnackbarOpen={this.props.onSnackbarOpen}
                        projectId={projectId}
                        resource={`${resource}/rules`}
                        serverBase={serverBase}
                        queryParams={{subject: ruleIndex}}
                    />
                }
            </FullscreenDialog>
        );
    }
}

ClassifiedObjectDialog.propTypes = {
    coveredObjectResource: PropTypes.string,
    disableAttributesMenu: PropTypes.bool,
    item: PropTypes.exact({
        id: PropTypes.number,
        name: PropTypes.shape({
            primary: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
            secondary: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
            toString: PropTypes.func
        }),
        traits: PropTypes.shape({
            originalDecision: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
            suggestedDecision: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
            certainty: PropTypes.number,
            indicesOfCoveringRules: PropTypes.number
        }),
        toFilter: PropTypes.func
    }),
    objectGlobalName: PropTypes.string,
    onClose: PropTypes.func,
    onSnackbarOpen: PropTypes.func,
    open: PropTypes.bool.isRequired,
    projectId: PropTypes.string,
    resource: PropTypes.string,
    serverBase: PropTypes.string
};

export default ClassifiedObjectDialog;