ToolJet/frontend/src/_ui/JSONTreeViewer/JSONNode.jsx
Arpit 7c7b011537
[inspector] followup to #2789 (#3027)
* followup to: 2789

* Revert "followup to: 2789"

This reverts commit 8a2b8b90a4.

* followup to: 2789
2022-05-11 15:01:12 +05:30

362 lines
12 KiB
JavaScript

import React from 'react';
import _ from 'lodash';
import cx from 'classnames';
import { ToolTip } from '@/_components/ToolTip';
import CopyToClipboardComponent from '@/_components/CopyToClipboard';
import { Popover } from 'react-bootstrap';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import JSONNodeObject from './JSONNodeObject';
import JSONNodeArray from './JSONNodeArray';
import JSONNodeValue from './JSONNodeValue';
import JSONNodeIndicator from './JSONNodeIndicator';
export const JSONNode = ({ data, ...restProps }) => {
const {
path,
shouldExpandNode,
currentNode,
selectedNode,
hoveredNode,
getCurrentPath,
getCurrentNodeType,
getLength,
toUseNodeIcons,
renderNodeIcons,
useIndentedBlock,
updateSelectedNode,
updateHoveredNode,
useActions,
enableCopyToClipboard,
getNodeShowHideComponents,
getOnSelectLabelDispatchActions,
expandWithLabels,
getAbsoluteNodePath,
actionsList,
updateParentState = () => null,
} = restProps;
const [expandable, set] = React.useState(() =>
typeof shouldExpandNode === 'function' ? shouldExpandNode(path, data) : shouldExpandNode
);
const [showHiddenOptionsForNode, setShowHiddenOptionsForNode] = React.useState(false);
const [showHiddenOptionButtons, setShowHiddenOptionButtons] = React.useState([]);
const [onSelectDispatchActions, setOnSelectDispatchActions] = React.useState([]);
React.useEffect(() => {
if (showHiddenOptionButtons) {
setShowHiddenOptionButtons(() => getNodeShowHideComponents(currentNode, path));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
if (useActions && currentNode) {
const onSelectDispatchActions = getOnSelectLabelDispatchActions(currentNode, path).filter(
(action) => action.onSelect
);
if (onSelectDispatchActions.length > 0) {
setOnSelectDispatchActions(onSelectDispatchActions);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedNode]);
const toggleExpandNode = (node) => {
if (expandable) {
updateSelectedNode(null);
} else {
updateSelectedNode(node, path);
}
set((prev) => !prev);
};
const onSelect = (data, currentNode, path) => {
const actions = onSelectDispatchActions;
actions.forEach((action) => action.dispatchAction(data, currentNode));
if (!expandWithLabels) {
updateSelectedNode(currentNode, path);
set(true);
}
};
const handleOnClickLabels = (data, currentNode, path) => {
if (expandWithLabels) {
toggleExpandNode(currentNode);
}
if (useActions) {
onSelect(data, currentNode, path);
}
};
const typeofCurrentNode = getCurrentNodeType(data);
const currentNodePath = getCurrentPath(path, currentNode);
const toExpandNode = (data instanceof Array || data instanceof Object) && !_.isEmpty(data);
const toShowNodeIndicator = (data instanceof Array || data instanceof Object) && typeofCurrentNode !== 'Function';
const numberOfEntries = getLength(typeofCurrentNode, data);
const toRenderSelector = (typeofCurrentNode === 'Object' || typeofCurrentNode === 'Array') && numberOfEntries > 0;
let $VALUE = null;
let $NODEType = null;
let $NODEIcon = null;
const checkSelectedNode = (_selectedNode, _currentNode, parent, toExpand) => {
if (selectedNode?.parent && parent) {
return _selectedNode.parent === parent && _selectedNode?.node === _currentNode && toExpand;
}
return toExpand && _selectedNode?.node === _currentNode;
};
const parent = path && typeof path?.length === 'number' ? path[path.length - 2] : null;
const applySelectedNodeStyles = toExpandNode
? checkSelectedNode(selectedNode, currentNode, parent, expandable)
: false;
React.useEffect(() => {
if (!expandable) {
updateSelectedNode(null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [expandable]);
React.useEffect(() => {
if (selectedNode?.node === currentNode) {
set(true);
}
}, [selectedNode, currentNode]);
React.useEffect(() => {
if (hoveredNode?.node === currentNode && hoveredNode?.parent === parent) {
setShowHiddenOptionsForNode(true);
}
return () => {
setShowHiddenOptionsForNode(false);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hoveredNode]);
if (toUseNodeIcons && currentNode) {
$NODEIcon = renderNodeIcons(currentNode);
}
switch (typeofCurrentNode) {
case 'String':
case 'Boolean':
case 'Number':
case 'Null':
case 'Undefined':
case 'Function':
$VALUE = <JSONNodeValue data={data} type={typeofCurrentNode} />;
$NODEType = <JSONNode.DisplayNodeLabel type={typeofCurrentNode} />;
break;
case 'Object':
$VALUE = <JSONNodeObject data={data} path={currentNodePath} {...restProps} />;
$NODEType = (
<JSONNode.DisplayNodeLabel type={'Object'}>
<span className="mx-1 fs-9 node-length-color">
{`${numberOfEntries} ${numberOfEntries > 1 ? 'entries' : 'entry'}`}{' '}
</span>
</JSONNode.DisplayNodeLabel>
);
break;
case 'Array':
$VALUE = <JSONNodeArray data={data} path={currentNodePath} {...restProps} />;
$NODEType = (
<JSONNode.DisplayNodeLabel type={'Array'}>
<span className="mx-1 fs-9 node-length-color">
{`${numberOfEntries} ${numberOfEntries > 1 ? 'items' : 'item'}`}{' '}
</span>
</JSONNode.DisplayNodeLabel>
);
break;
default:
$VALUE = <span>{String(data)}</span>;
$NODEType = typeofCurrentNode;
}
let $key = (
<span
onClick={() => toExpandNode && handleOnClickLabels(data, currentNode, path)}
style={{ marginTop: '1px', cursor: 'pointer', textTransform: 'none' }}
className={cx('node-key fs-12 mx-0 badge badge-outline', {
'color-primary': applySelectedNodeStyles && !showHiddenOptionsForNode,
'hovered-node': showHiddenOptionsForNode,
'node-key-outline': !applySelectedNodeStyles && !showHiddenOptionsForNode,
})}
>
{String(currentNode)}
</span>
);
if (!currentNode) {
return $VALUE;
}
const shouldDisplayIntendedBlock =
useIndentedBlock && expandable && (typeofCurrentNode === 'Object' || typeofCurrentNode === 'Array');
function moreActionsPopover(actions) {
//Todo: For adding more actions to the menu popover!
const darkMode = localStorage.getItem('darkMode') === 'true';
return (
<Popover
id="popover-basic popover-positioned-right json-tree-popover"
style={{ maxWidth: '350px', padding: '0px' }}
className={`shadow ${darkMode && 'popover-dark-themed theme-dark'}`}
>
<div className="list-group">
{actions?.map((action, index) => (
<span
key={index}
type="button"
className="list-group-item list-group-item-action popover-more-actions"
aria-current="true"
onClick={() => {
action.dispatchAction(data, currentNode);
updateParentState();
}}
>
{action.name}
</span>
))}
</div>
</Popover>
);
}
const renderHiddenOptionsForNode = () => {
const moreActions = actionsList.filter((action) => action.for === 'all')[0];
const renderOptions = () => {
if (!useActions || showHiddenOptionButtons?.length === 0) return null;
return showHiddenOptionButtons?.map((actionOption, index) => {
const { name, icon, src, iconName, dispatchAction, width = 12, height = 12 } = actionOption;
if (icon) {
return (
<ToolTip key={`${name}-${index}`} message={`${name} ${currentNode}`}>
<span
style={{ height: '13px', width: '13px' }}
className="mx-1"
onClick={() => dispatchAction(data, currentNode)}
>
<img src={src ?? `/assets/images/icons/${iconName}.svg`} width={width} height={height} />
</span>
</ToolTip>
);
}
});
};
return (
<div style={{ fontSize: '9px', marginTop: '0px' }} className="d-flex end-0 position-absolute">
{enableCopyToClipboard && (
<CopyToClipboardComponent data={currentNodePath} path={true} callback={getAbsoluteNodePath} />
)}
{renderOptions()}
{moreActions.actions?.length > 0 && (
<OverlayTrigger
rootClose={true}
rootCloseEvent="mousedown"
trigger="click"
placement={'right'}
overlay={moreActionsPopover(moreActions?.actions)}
>
<span>
<ToolTip message={'More actions'}>
<svg
xmlns="http://www.w3.org/2000/svg"
className="icon-tabler icon-tabler-dots-vertical"
width="13"
height="13"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="#2c3e50"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="1" />
<circle cx="12" cy="19" r="1" />
<circle cx="12" cy="5" r="1" />
</svg>
</ToolTip>
</span>
</OverlayTrigger>
)}
</div>
);
};
return (
<div
className={cx('d-flex row-flex mt-1 font-monospace container-fluid px-1', {
'json-node-element': !expandable,
})}
>
<div className={`json-tree-icon-container mx-2 ${applySelectedNodeStyles && 'selected-node'}`}>
<JSONNodeIndicator
toExpand={expandable}
toShowNodeIndicator={toShowNodeIndicator}
handleToggle={toggleExpandNode}
typeofCurrentNode={typeofCurrentNode}
currentNode={currentNode}
isSelected={selectedNode?.node === currentNode}
toExpandNode={toExpandNode}
data={data}
path={currentNodePath}
toExpandWithLabels={expandWithLabels}
toggleWithLabels={handleOnClickLabels}
/>
</div>
<div
style={{ width: 'inherit' }}
className={`${shouldDisplayIntendedBlock && 'group-border'} ${applySelectedNodeStyles && 'selected-node'}`}
onMouseEnter={() => updateHoveredNode(currentNode, currentNodePath)}
onMouseLeave={() => updateHoveredNode(null)}
>
<div
className={cx('d-flex', {
'group-object-container': shouldDisplayIntendedBlock,
'mx-2': typeofCurrentNode !== 'Object' && typeofCurrentNode !== 'Array',
})}
>
{$NODEIcon && <div className="json-tree-icon-container">{$NODEIcon}</div>}
{$key} {$NODEType}
{!toExpandNode && !expandable && !toRenderSelector ? $VALUE : null}
<div className="action-icons-group">{showHiddenOptionsForNode && renderHiddenOptionsForNode()}</div>
</div>
{toRenderSelector && (toExpandNode && !expandable ? null : $VALUE)}
</div>
</div>
);
};
const DisplayNodeLabel = ({ type = '', children }) => {
if (type === 'Null' || type === 'Undefined') {
return null;
}
return (
<>
<span className="mx-1 fs-10 node-type">{type}</span>
{children}
</>
);
};
JSONNode.DisplayNodeLabel = DisplayNodeLabel;