Skip to content

Commit

Permalink
Embedded method tree conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffibm committed Jan 30, 2024
1 parent 257bf31 commit 444ef60
Show file tree
Hide file tree
Showing 9 changed files with 423 additions and 0 deletions.
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

0 comments on commit 444ef60

Please sign in to comment.