Commit c281d62c authored by Patrik Meijer's avatar Patrik Meijer
Browse files

Can create Connections and Pointers (and almost sets members) in GraphEditor

parent a4ef4759
Loading
Loading
Loading
Loading
+0 −101
Original line number Diff line number Diff line
/* globals document */
/**
 * TODO: Consider moving this outside of the GraphEditor and let it be passed as child instead.
 * @author pmeijer / https://github.com/pmeijer
 */
import React, {Component} from 'react';
import PropTypes from 'prop-types';

import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';

export default class ContextMenu extends Component {
    static propTypes = {
        gmeClient: PropTypes.object.isRequired,
        nodeId: PropTypes.string.isRequired, // FIXME: should be nodeIds
        eventX: PropTypes.number.isRequired,
        eventY: PropTypes.number.isRequired,
        onClose: PropTypes.func.isRequired,
        setActiveNode: PropTypes.func.isRequired,
        createPointer: PropTypes.func.isRequired,

        readOnly: PropTypes.bool,
    };

    static defaultProps = {
        readOnly: false,
    }

    setActiveNode = () => {
        const {nodeId, onClose, setActiveNode} = this.props;

        setActiveNode(nodeId);
        onClose();
    }

    deleteNode = () => {
        const {gmeClient, nodeId, onClose} = this.props;

        gmeClient.deleteNode(nodeId);

        onClose();
    }

    getCreatePointerFunc = (ptrName) => {
        const {createPointer, nodeId} = this.props;
        return () => {
            createPointer(nodeId, ptrName);
        };
    }

    render() {
        const {
            gmeClient,
            nodeId,
            eventX,
            eventY,
            readOnly,
            onClose,
        } = this.props;

        // console.log(data);
        const menuItems = [];
        const nodeObj = gmeClient.getNode(nodeId);
        const metaNodeObj = gmeClient.getNode(nodeObj.getMetaTypeId());

        const pointerNames = metaNodeObj.getValidPointerNames();

        menuItems.push((
            <MenuItem key="meta-type" onClick={onClose} disabled={readOnly}>
                {`<<${metaNodeObj.getAttribute('name')}>>`}
            </MenuItem>));

        menuItems.push((
            <MenuItem key="open-sub-system" onClick={this.setActiveNode} >
                Open Subsystem ...
            </MenuItem>));

        pointerNames.forEach((ptr) => {
            menuItems.push((
                <MenuItem key={`create-pointer-${ptr}`} onClick={this.getCreatePointerFunc(ptr)} >
                    {`Create pointer ${ptr} from here ...`}
                </MenuItem>));
        });

        return (
            <Menu
                id="simple-menu"
                anchorEl={document.body}
                style={{
                    position: 'absolute',
                    top: eventY - (document.body.clientHeight / 2),
                    left: eventX,
                }}
                open
                onClose={onClose}
            >
                {menuItems}
            </Menu>
        );
    }
}
+10 −3
Original line number Diff line number Diff line
@@ -29,9 +29,10 @@ const DEFAULT_STYLES = [
    },
    {
        selector: 'node.aux-node',
        css: {
        style: {
            width: 6,
            height: 6,
            'background-color': 'rgb(174, 219, 255)',
        },
    },
    {
@@ -89,9 +90,15 @@ const DEFAULT_STYLES = [
        },
    },
    {
        selector: 'node.valid-pointer-target',
        selector: 'node.valid-action-target',
        style: {
            'background-color': 'rgba(185, 236, 185, 0.64)',
        },
    },
    {
        selector: 'node.invalid-action-target',
        style: {
            'background-color': 'rgb(66, 220, 244)',
            'background-color': 'rgba(236, 150, 185, 0.64)',
        },
    },
    {
+88 −33
Original line number Diff line number Diff line
@@ -19,7 +19,7 @@ import FilterIcon from '@material-ui/icons/FilterList';
import ViewModuleIcon from '@material-ui/icons/ViewModule';

import ReactCytoscape from './ReactCytoscape';
import ContextMenu from './ContextMenu';
import SimpleActionMenu from '../SimpleActionMenu';
import FilterSelector from './FilterSelector';

import DEFAULT_STYLES from './DefaultStyles';
@@ -138,7 +138,7 @@ export default class GraphEditor extends Component {
            return res;
        })(),
        showFilterSelector: false,
        createPointer: null,
        creatingNew: null,
    };

    componentWillReceiveProps({activeNode}) {
@@ -167,7 +167,7 @@ export default class GraphEditor extends Component {
            activeSelection,
            nodes,
        } = this.props;
        const {activeFilters, createPointer} = this.state;
        const {activeFilters, creatingNew} = this.state;
        // https://github.com/ybarukh/react-cytoscape/blob/master/sample/app/Graph.js
        // http://js.cytoscape.org/#notation/elements-json
        const result = {
@@ -241,8 +241,8 @@ export default class GraphEditor extends Component {
${activeSelection.includes(id) ? ' in-active-selection' : ''}`,
                    };

                    if (createPointer && createPointer.target === id) {
                        edgeData.classes += ' valid-pointer-target';
                    if (creatingNew && creatingNew.target === id) {
                        edgeData.classes += creatingNew.isValid ? ' valid-action-target' : ' invalid-action-target';
                    }

                    if (hasEdgeTargets(childData.pointers.src, childData.pointers.dst)) {
@@ -285,8 +285,8 @@ ${activeSelection.includes(id) ? 'in-active-selection' : ''}`,
                        });
                    }

                    if (createPointer && createPointer.target === id) {
                        cytoData.classes += ' valid-pointer-target';
                    if (creatingNew && creatingNew.target === id) {
                        cytoData.classes += creatingNew.isValid ? ' valid-action-target' : ' invalid-action-target';
                    }
                }

@@ -414,10 +414,25 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`,
    startNewPointer = (nodeId, ptrName) => {
        this.setState({
            showNodeMenu: false,
            createPointer: {
            creatingNew: {
                type: 'pointer',
                nodeId,
                ptrName,
                id: ptrName,
                target: null,
                isValid: null,
            },
        });
    };

    startNewConnection = (nodeId, connTypeId) => {
        this.setState({
            showNodeMenu: false,
            creatingNew: {
                type: 'connection',
                nodeId,
                id: connTypeId,
                target: null,
                isValid: null,
            },
        });
    };
@@ -459,22 +474,50 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`,

        this.cy.on('vclick', 'node', (e) => {
            const {setActiveSelection, gmeClient} = this.props;
            const {createPointer} = this.state;
            const {creatingNew} = this.state;

            // This is async :+1:
            this.setState({creatingNew: null});

            if (typeof e.target.id === 'function') {
                if (createPointer && createPointer.target === e.target.id()) {
                    this.setState({createPointer: null});
                    gmeClient.setPointer(createPointer.nodeId, createPointer.ptrName, createPointer.target);
                const nodeId = e.target.id();
                if (creatingNew && creatingNew.target === nodeId && creatingNew.isValid) {
                    if (creatingNew.type === 'pointer') {
                        gmeClient.setPointer(creatingNew.nodeId, creatingNew.id, creatingNew.target);
                    } else if (creatingNew.type === 'set') {
                        // FIXME: This uses the shuffled client arguments for addMember!
                        gmeClient.addMember(creatingNew.nodeId, creatingNew.target, creatingNew.id);
                    } else if (creatingNew.type === 'connection') {
                        const gmeNode = gmeClient.getNode(nodeId);
                        if (gmeNode) {
                            const commonParentId = gmeNode.getCommonParentId(creatingNew.nodeId);
                            gmeClient.startTransaction();
                            const connId = gmeClient.createNode({
                                baseId: creatingNew.id,
                                parentId: commonParentId,
                            });

                            gmeClient.setPointer(connId, 'src', creatingNew.nodeId);
                            gmeClient.setPointer(connId, 'dst', creatingNew.target);

                            gmeClient.completeTransaction();

                            setTimeout(() => {
                                setActiveSelection([connId]);
                            });
                        }
                    } else {
                        console.error('Unexpected createNew type', JSON.stringy(creatingNew));
                    }
                } else if (e.target.hasClass('aux-node') === false) {
                    this.setState({
                        showNodeMenu: {
                            id: e.target.id(),
                            data: e.target.data(),
                            position: {
                                x: e.originalEvent.clientX,
                                y: e.originalEvent.clientY,
                            },
                        },
                        createPointer: null,
                    });

                    setTimeout(() => {
@@ -487,23 +530,35 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`,
        });

        this.cy.on('mouseover', 'node', (e) => {
            const {createPointer} = this.state;
            const {creatingNew} = this.state;
            const {gmeClient} = this.props;
            if (createPointer
                && typeof e.target.id === 'function'
                && e.target.hasClass('aux-node') === false) {
                const gmeNode = gmeClient.getNode(e.target.id());
                if (gmeNode && gmeNode.isValidTargetOf(createPointer.nodeId, createPointer.ptrName)
                    && createPointer.target !== e.target.id()) {

            const nodeId = e.target.id();
            const gmeNode = gmeClient.getNode(nodeId);
            if (!gmeNode) {
                return;
            }

            if (creatingNew && creatingNew.target !== nodeId) {
                let isValid = false;
                if (creatingNew.type === 'pointer') {
                    isValid = gmeNode.isValidTargetOf(creatingNew.nodeId, creatingNew.id);
                } else if (creatingNew.type === 'set') {
                    isValid = gmeNode.isValidMemberOf(creatingNew.nodeId, creatingNew.id);
                } else if (creatingNew.type === 'connection') {
                    isValid = gmeNode.isValidTargetOf(creatingNew.id, 'dst');
                }

                this.setState({
                        createPointer: {
                            nodeId: createPointer.nodeId,
                            ptrName: createPointer.ptrName,
                            target: e.target.id(),
                    creatingNew: {
                        type: creatingNew.type,
                        id: creatingNew.id, // connection meta-id, ptr-name or set-name.
                        nodeId: creatingNew.nodeId,
                        target: nodeId,
                        isValid,
                    },
                });
            }
            }
        });
    }

@@ -615,17 +670,17 @@ ${activeSelection.includes(edgeId) ? ' in-active-selection' : ''}`,
                />
                {showNodeMenu && showNodeMenu.id === activeSelection[0]
                    ? (
                        <ContextMenu
                        <SimpleActionMenu
                            gmeClient={gmeClient}
                            nodeId={showNodeMenu.id}
                            data={showNodeMenu.data}
                            eventX={showNodeMenu.position.x}
                            eventY={showNodeMenu.position.y}
                            onClose={() => {
                                this.setState({showNodeMenu: null});
                            }}
                            createPointer={this.startNewPointer}
                            setActiveNode={setActiveNode}
                            onSetPointerTarget={this.startNewPointer}
                            onCreateConnection={this.startNewConnection}
                            onSetActiveNode={setActiveNode}
                        />) : null}

                <FilterSelector
+182 −0
Original line number Diff line number Diff line
/* globals document */
/**
 * TODO: Consider moving this outside of the GraphEditor and let it be passed as child instead.
 * @author pmeijer / https://github.com/pmeijer
 */
import React, {Component} from 'react';
import PropTypes from 'prop-types';

import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';

import OpenIcon from '@material-ui/icons/ArrowDownward';
import DeleteIcon from '@material-ui/icons/Clear';
import PointerIcon from '@material-ui/icons/TrendingFlat';
import SetIcon from '@material-ui/icons/CallSplit';
import ConnectionIcon from '@material-ui/icons/SettingsEthernet';
import NodeIcon from '@material-ui/icons/FiberManualRecord';

function noop() {}

export default class SimpleActionMenu extends Component {
    static propTypes = {
        gmeClient: PropTypes.object.isRequired,
        nodeId: PropTypes.string.isRequired,
        eventX: PropTypes.number.isRequired,
        eventY: PropTypes.number.isRequired,
        onClose: PropTypes.func.isRequired,

        allowDeletion: PropTypes.bool,
        onSetActiveNode: PropTypes.func,
        onSetPointerTarget: PropTypes.func,
        onAddSetMember: PropTypes.func,
        onCreateConnection: PropTypes.func,
        onCreateChild: PropTypes.func,
    };

    static defaultProps = {
        allowDeletion: true,

        onSetActiveNode: noop,
        onSetPointerTarget: noop,
        onAddSetMember: noop,
        onCreateConnection: noop,
        onCreateChild: noop,
    }

    setActiveNode = () => {
        const {nodeId, onSetActiveNode} = this.props;

        onSetActiveNode(nodeId);
    }

    deleteNode = () => {
        const {gmeClient, nodeId, onClose} = this.props;

        gmeClient.deleteNode(nodeId);

        onClose();
    }

    getSetPointerFunc = (ptrName) => {
        const {onSetPointerTarget, nodeId} = this.props;
        return () => {
            onSetPointerTarget(nodeId, ptrName);
        };
    }

    getAddMemberFunc = (setName) => {
        const {onAddSetMember, nodeId} = this.props;
        return () => {
            onAddSetMember(nodeId, setName);
        };
    }

    getCreateConnectionFunc = (connectionTypeId) => {
        const {onCreateConnection, nodeId} = this.props;
        return () => {
            onCreateConnection(nodeId, connectionTypeId);
        };
    }

    render() {
        const {
            gmeClient,
            nodeId,
            eventX,
            eventY,
            allowDeletion,

            onSetActiveNode,
            onSetPointerTarget,
            onAddSetMember,
            onCreateConnection,
            onCreateChild,

            onClose,
        } = this.props;

        const menuItems = [];
        const nodeObj = gmeClient.getNode(nodeId);
        const metaNodeObj = gmeClient.getNode(nodeObj.getMetaTypeId());
        const isReadOnly = nodeObj.isReadOnly();

        if (onSetActiveNode !== noop) {
            menuItems.push((
                <MenuItem key="open-sub-system" onClick={this.setActiveNode}>
                    <ListItemIcon>
                        <OpenIcon/>
                    </ListItemIcon>
                    <ListItemText>
                        {`Open ${nodeObj.getAttribute('name')}`}
                    </ListItemText>
                </MenuItem>));
        }

        if (onSetPointerTarget !== noop && isReadOnly === false) {
            const pointerNames = metaNodeObj.getValidPointerNames();
            pointerNames.forEach((name) => {
                menuItems.push((
                    <MenuItem key={`create-pointer-${name}`} onClick={this.getSetPointerFunc(name)}>
                        <ListItemIcon>
                            <PointerIcon/>
                        </ListItemIcon>
                        <ListItemText>
                            {`Create pointer ${name} from here ...`}
                        </ListItemText>
                    </MenuItem>));
            });
        }

        if (onAddSetMember !== noop && isReadOnly === false) {
            const setNames = metaNodeObj.getValidSetNames();

            setNames.forEach((name) => {
                menuItems.push((
                    <MenuItem key={`add-set-member-${name}`} onClick={this.getAddMemberFunc(name)}>
                        <ListItemIcon>
                            <SetIcon/>
                        </ListItemIcon>
                        <ListItemText>
                            {`Add new member to set ${name} ...`}
                        </ListItemText>
                    </MenuItem>));
            });
        }

        if (onCreateConnection !== noop && isReadOnly === false) {
            gmeClient.getAllMetaNodes()
                .filter(metaNode => metaNode.isConnection() && nodeObj.isValidTargetOf(metaNode.getId(), 'src'))
                .map(metaNode => ({name: metaNode.getFullyQualifiedName(), id: metaNode.getId()}))
                .forEach((info) => {
                    menuItems.push((
                        <MenuItem key={`create-new-conn-${info}`} onClick={this.getCreateConnectionFunc(info.id)}>
                            <ListItemIcon>
                                <ConnectionIcon/>
                            </ListItemIcon>
                            <ListItemText>
                                {`Create connection ${info.name} from here...`}
                            </ListItemText>
                        </MenuItem>));
                });
        }

        return (
            <Menu
                id="simple-menu"
                anchorEl={document.body}
                style={{
                    position: 'absolute',
                    top: eventY - (document.body.clientHeight / 2),
                    left: eventX,
                }}
                open
                onClose={onClose}
            >
                {menuItems}
            </Menu>
        );
    }
}
+1 −0
Original line number Diff line number Diff line
export {default} from './SimpleActionMenu';