Skip to content

[WIP] Embedded method tree conversion #9052

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions app/javascript/components/AeInlineMethods/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Modal, Button, ModalBody } from 'carbon-components-react';
import MiqTree from '../MiqTreeView';

/** Component to render a tree and to select an embedded method. */
const AeInlineMethod = ({ type }) => {
const [data, setData] = useState({
isModalOpen: false,
selectedNode: undefined,
list: [],
});

/** Function to show/hide the modal. */
const showModal = (status) => {
setData({
...data,
isModalOpen: status,
});
};

/** Function to render the Add method button. */
const renderAddButton = () => (
<Button
id="add-method"
kind="primary"
title={__('Add Method')}
onClick={() => showModal(true)}
size="sm"
>
{__('Add method')}
</Button>
);

console.log(data);

const renderList = () => (data.list.map((item) => (
<div key={item.key}>
<div>{item.fqname}</div>
</div>
)));

return (
<div>
{renderAddButton()}
{renderList()}
<Modal
primaryButtonDisabled={data.selectedNode === undefined}
size="lg"
modalHeading={__('Select item')}
open={data.isModalOpen}
primaryButtonText={__('OK')}
secondaryButtonText={__('Cancel')}
onRequestClose={() => showModal(false)}
onRequestSubmit={() => {
console.log('on onRequestSubmit');
setData({
...data,
list: data.list.push(data.selectedNode),
});
showModal(false);
}}
onSecondarySubmit={() => {
console.log('on onSecondarySubmit');
showModal(false);
}}
>
<ModalBody>
{
data.isModalOpen
&& (
<MiqTree
type={type}
onNodeSelect={(item) => {
setData({
...data,
selectedNode: item,
});
}}
/>
)
}
</ModalBody>
</Modal>

</div>
);
};

export default AeInlineMethod;

AeInlineMethod.propTypes = {
type: PropTypes.string.isRequired,
};
46 changes: 46 additions & 0 deletions app/javascript/components/MiqTreeView/MiqTreeChildNode.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable import/no-cycle */
import React from 'react';
import PropTypes from 'prop-types';
import MiqTreeNode from './MiqTreeNode';

/** A component to render the parent node of the tree. */
const MiqTreeChildNode = ({
node, onSelect, selectedNode, selectKey,
}) => (
<div className="tree-row child-tree intend-right" key={node.key}>
{
node.nodes.map((item) => (
<MiqTreeNode
key={item.key}
node={item}
selectedNode={selectedNode}
selectKey={selectKey}
onSelect={(childItem) => onSelect(childItem)}
/>
))
}
</div>
);

export default MiqTreeChildNode;

MiqTreeChildNode.propTypes = {
node: PropTypes.shape({
key: PropTypes.string,
nodes: PropTypes.arrayOf(PropTypes.any),
state: PropTypes.shape({
expanded: PropTypes.bool,
}),
}).isRequired,
selectKey: PropTypes.string.isRequired,
onSelect: PropTypes.func.isRequired,
selectedNode: PropTypes.shape({
key: PropTypes.string,
}),
};

MiqTreeChildNode.defaultProps = {
selectedNode: undefined,
};
54 changes: 54 additions & 0 deletions app/javascript/components/MiqTreeView/MiqTreeNode.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* eslint-disable import/no-cycle */
import React from 'react';
import PropTypes from 'prop-types';
import MiqTreeParentNode from './MiqTreeParentNode';
import MiqTreeChildNode from './MiqTreeChildNode';

/** A Recursive Functional component to render the Tree and its child nodes. */
const MiqTreeNode = ({
node, selectedNode, selectKey, onSelect,
}) => {
const isSelected = (selectedNode && selectedNode.key === node.key) || false;

return (
<div key={node.key}>
<MiqTreeParentNode
node={node}
isSelected={isSelected}
selectKey={selectKey}
onSelect={(parentItem) => onSelect(parentItem)}
/>
{
node.state.expanded && node.nodes && node.nodes.length > 0 && (
<MiqTreeChildNode
node={node}
onSelect={(childItem) => onSelect(childItem)}
selectedNode={selectedNode}
selectKey={selectKey}
/>
)
}
</div>
);
};

export default MiqTreeNode;

MiqTreeNode.propTypes = {
node: PropTypes.shape({
key: PropTypes.string,
nodes: PropTypes.arrayOf(PropTypes.any),
state: PropTypes.shape({
expanded: PropTypes.bool,
}),
}).isRequired,
selectKey: PropTypes.string.isRequired,
onSelect: PropTypes.func.isRequired,
selectedNode: PropTypes.shape({
key: PropTypes.string,
}),
};

MiqTreeNode.defaultProps = {
selectedNode: undefined,
};
49 changes: 49 additions & 0 deletions app/javascript/components/MiqTreeView/MiqTreeParentNode.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { CaretRight16, CaretDown16, CheckmarkFilled16 } from '@carbon/icons-react';
import { selectableItem } from './helper';

/** A component to render the parent node of the tree. */
const MiqTreeParentNode = ({
node, selectKey, isSelected, onSelect,
}) => {
/** Function to render the down and right caret. */
const renderCaret = (item) => {
if (!item) {
return undefined;
}
if (selectableItem(item, selectKey) || !item.lazyLoad) {
return undefined;
}
return item.state.expanded ? <CaretDown16 className="tree-caret" /> : <CaretRight16 className="tree-caret" />;
};

return (
<div className={classNames('tree-row parent-tree', isSelected && 'selected-node')} onClick={() => onSelect(node)}>
{renderCaret(node)}
<div className="tree-icon"><i className={node.icon} /></div>
<div className="tree-text">{node.text}</div>
{isSelected && <CheckmarkFilled16 className="selected-node-check" />}
</div>
);
};

export default MiqTreeParentNode;

MiqTreeParentNode.propTypes = {
node: PropTypes.shape({
icon: PropTypes.string,
text: PropTypes.string,
key: PropTypes.string,
nodes: PropTypes.arrayOf(PropTypes.any),
state: PropTypes.shape({
expanded: PropTypes.bool,
}),
}).isRequired,
selectKey: PropTypes.string.isRequired,
isSelected: PropTypes.bool.isRequired,
onSelect: PropTypes.func.isRequired,
};
19 changes: 19 additions & 0 deletions app/javascript/components/MiqTreeView/helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const TREE_CONFIG = {
aeInlineMethod: {
url: '/tree/automate_inline_methods',
selectKey: 'aem',
},
};

/** Function to find the selected item from the tree data. */
export const findNodeByKey = (array, keyToFind) => {
const flattenedArray = array.flatMap((item) => [item, ...(item.nodes || [])]);
const foundNode = flattenedArray.find((item) => item.key === keyToFind);

return foundNode || flattenedArray
.filter((item) => item.nodes && item.nodes.length > 0)
.map((item) => findNodeByKey(item.nodes, keyToFind))
.find(Boolean);
};

export const selectableItem = (child, selectKey) => child.key.split('-')[0] === selectKey;
112 changes: 112 additions & 0 deletions app/javascript/components/MiqTreeView/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Loading } from 'carbon-components-react';
import MiqTreeNode from './MiqTreeNode';
import { TREE_CONFIG, selectableItem, findNodeByKey } from './helper';
import './style.scss';

const MiqTree = ({ type, onNodeSelect }) => {
const treeType = TREE_CONFIG[type];

const [data, setData] = useState({
list: undefined,
isLoading: true,
selectedNode: undefined,
});

/** Function to update the data. */
const updateData = (others) => {
setData({
...data,
...others,
});
};

/** A request is made to fetch the initial data during component load. */
useEffect(() => {
http.get(treeType.url)
.then((response) => updateData({ list: response, isLoading: false }));
}, []);

useEffect(() => {
onNodeSelect(data.selectedNode);
}, [data.selectedNode]);

/** Function to select a node from tree. This triggers the useEffect. */
const selectNode = (node) => {
const selectedNode = (data.selectedNode && data.selectedNode.key === node.key) ? undefined : node;
updateData({ selectedNode });
};

/** Function to handle show the children of a node
* if child nodes are available, just expand the tree.
* else, request an API to fetch the child nodes and update the results.
*/
const expandTree = (item, node) => {
if (item.nodes && item.nodes.length > 0) {
item.state.expanded = true;
updateData({ list: [...data.list] });
} else {
http.get(`${treeType.url}?id=${node.key}`)
.then((response) => {
item.nodes = response;
item.state.expanded = true;
updateData({ list: [...data.list] });
});
}
};

/** Function to collapse the tree to hide the children */
const collapseTree = (item) => {
item.state.expanded = false;
updateData({ list: [...data.list] });
};

/** Function to expand/collapse the tree. */
const toggleTree = (node) => {
const item = findNodeByKey(data.list, node.key);
if (item) {
if (node.state.expanded) {
collapseTree(item);
} else {
expandTree(item, node);
}
}
};

/** Function to handle the click events of tree node. */
const loadSelectedNode = (node) => (selectableItem(node, treeType.selectKey)
? selectNode(node)
: toggleTree(node));

/** Function to render the tree contents. */
const renderTree = (list) => (list && list.map((child) => (
<MiqTreeNode
key={child.key}
node={child}
selectedNode={data.selectedNode}
selectKey={treeType.selectKey}
onSelect={(node) => loadSelectedNode(node)}
/>
)));

/** Function to render the modal contents. */
const renderTreeContent = () => ((data.list && data.list.length > 0) ? renderTree(data.list) : undefined);

return (
<div>
{
data.isLoading
? <Loading active small withOverlay={false} className="loading" />
: renderTreeContent()
}
</div>
);
};

export default MiqTree;

MiqTree.propTypes = {
type: PropTypes.string.isRequired,
onNodeSelect: PropTypes.func.isRequired,
};
Loading