codekingpro/portable-devtools
114k
1import { Checkbox } from '@mui/material';2import { makeStyles } from '@mui/styles';3import clsx from 'clsx';4import gettext from 'sources/gettext';5import React, { useEffect, useRef } from 'react';6import { Tree } from 'react-arborist';7import AutoSizer from 'react-virtualized-auto-sizer';8import ExpandMoreIcon from '@mui/icons-material/ExpandMore';9import ChevronRightIcon from '@mui/icons-material/ChevronRight';10import PropTypes from 'prop-types';11import IndeterminateCheckBoxIcon from '@mui/icons-material/IndeterminateCheckBox';12import EmptyPanelMessage from '../components/EmptyPanelMessage';13import CheckBoxIcon from '@mui/icons-material/CheckBox';14 15 16const useStyles = makeStyles((theme) => ({17 node: {18 display: 'inline-block',19 paddingLeft: '1.5rem',20 height: '100%',21 },22 checkboxStyle: {23 fill: theme.palette.primary.main24 },25 tree: {26 background: theme.palette.background.default,27 height: '100%',28 width: '100%',29 display: 'flex',30 flexDirection: 'column',31 flex: 1,32 },33 focusedNode: {34 background: theme.palette.primary.light,35 },36 leafNode: {37 marginLeft: '1.5rem'38 },39}));40 41export const PgTreeSelectionContext = React.createContext();42 43export default function PgTreeView({ data = [], hasCheckbox = false, selectionChange = null}) {44 let classes = useStyles();45 let treeData = data;46 const treeObj = useRef();47 const treeContainerRef = useRef();48 const [selectedCheckBoxNodes, setSelectedCheckBoxNodes] = React.useState([]);49 50 const onSelectionChange = () => {51 let selectedChNodes = treeObj.current.selectedNodes;52 if (hasCheckbox) {53 let selectedChildNodes = [];54 55 treeObj.current.selectedNodes.forEach((node) => {56 if(node.isInternal && !node.isOpen) {57 node.children.forEach((ch)=>{58 if(ch.data.isSelected && ch.isLeaf && !selectedChildNodes.includes(ch.id)) {59 selectedChildNodes.push(ch.id);60 selectedChNodes.push(ch);61 }62 });63 }64 selectedChildNodes.push(node.id);65 });66 setSelectedCheckBoxNodes(selectedChildNodes);67 }68 69 selectionChange?.(selectedChNodes);70 };71 72 return (<>73 { treeData.length > 0 ?74 <PgTreeSelectionContext.Provider value={selectedCheckBoxNodes}>75 <div ref={(containerRef) => treeContainerRef.current = containerRef} className={clsx(classes.tree)}>76 <AutoSizer>77 {({ width, height }) => (78 <Tree79 ref={(obj) => {80 treeObj.current = obj;81 }}82 width={isNaN(width) ? 100 : width}83 height={isNaN(height) ? 100 : height}84 data={treeData}85 disableDrag={true}86 disableDrop={true}87 dndRootElement={treeContainerRef.current}88 >89 {90 (props) => <Node onNodeSelectionChange={onSelectionChange} hasCheckbox={hasCheckbox} {...props}></Node>91 }92 </Tree>93 )}94 </AutoSizer>95 </div>96 </PgTreeSelectionContext.Provider>97 :98 <EmptyPanelMessage text={gettext('No objects are found to display')}/>99 }100 </>101 );102}103 104PgTreeView.propTypes = {105 data: PropTypes.array,106 selectionChange: PropTypes.func,107 hasCheckbox: PropTypes.bool,108};109 110function Node({ node, style, tree, hasCheckbox, onNodeSelectionChange}) {111 const classes = useStyles();112 const pgTreeSelCtx = React.useContext(PgTreeSelectionContext);113 const [isSelected, setIsSelected] = React.useState(pgTreeSelCtx.includes(node.id) || node.data?.isSelected);114 const [isIndeterminate, setIsIndeterminate] = React.useState(node?.parent.level==0);115 116 useEffect(()=>{117 setIsIndeterminate(node.data.isIndeterminate);118 }, [node?.data?.isIndeterminate]);119 120 121 useEffect(()=>{122 if(isSelected){123 if(!pgTreeSelCtx.includes(node.id)){124 tree.selectMulti(node.id);125 onNodeSelectionChange();126 }127 }128 }, [isSelected]);129 130 131 const onCheckboxSelection = (e) => {132 if (hasCheckbox) {133 setIsSelected(e.currentTarget.checked);134 node.data.isSelected = e.currentTarget.checked;135 if (e.currentTarget.checked) {136 node.selectMulti(node.id);137 if (!node.isLeaf) {138 node.data.isIndeterminate = false;139 selectAllChild(node, tree, 'checkbox', pgTreeSelCtx);140 } else if (node?.parent) {141 checkAndSelectParent(node);142 }143 144 if(node?.level == 0) {145 node.data.isIndeterminate = false;146 }147 node.focus();148 } else {149 node.deselect(node);150 if (!node.isLeaf) {151 deselectAllChild(node);152 }153 154 if(node?.parent){155 node.parent.data.isIndeterminate = false;156 delectPrentNode(node.parent);157 }158 }159 }160 tree.scrollTo(node.id, 'center');161 onNodeSelectionChange();162 };163 164 const onSelect = (e) => {165 node.focus();166 e.stopPropagation();167 };168 169 const onKeyDown = (e) => {170 if(e.code == 'Enter') {171 onSelect(e);172 }173 };174 175 return (176 <div style={style} className={clsx(node.isFocused ? classes.focusedNode : '')} onClick={onSelect} onKeyDown={onKeyDown}>177 <CollectionArrow node={node} tree={tree} selectedNodeIds={pgTreeSelCtx} />178 {179 hasCheckbox ? <Checkbox style={{ padding: 0 }} color="primary" className={clsx(!node.isInternal ? classes.leafNode: null)}180 checked={isSelected}181 checkedIcon={isIndeterminate ? <IndeterminateCheckBoxIcon style={{height: '1.4rem'}} />: <CheckBoxIcon style={{height: '1.4rem'}} />}182 onChange={onCheckboxSelection}/> :183 <span className={clsx(node.data.icon)}></span>184 }185 <div className={clsx(node.data.icon, classes.node)}>{node.data.name}</div>186 </div>187 );188}189 190Node.propTypes = {191 node: PropTypes.object,192 style: PropTypes.any,193 tree: PropTypes.object,194 hasCheckbox: PropTypes.bool,195 onNodeSelectionChange: PropTypes.func196};197 198function CollectionArrow({ node, tree, selectedNodeIds }) {199 const toggleNode = () => {200 node.isInternal && node.toggle();201 if (node.isSelected && node.isOpen) {202 node.data.isSelected = true;203 selectAllChild(node, tree, 'expand', selectedNodeIds);204 }205 };206 return (207 <span onClick={toggleNode} onKeyDown={()=>{/* handled by parent */}}>208 {node.isInternal && node?.children.length > 0 ? <ToggleArrowIcon node={node} /> : null}209 </span>210 );211}212 213CollectionArrow.propTypes = {214 node: PropTypes.object,215 tree: PropTypes.object,216 selectedNodeIds: PropTypes.array217};218 219 220function ToggleArrowIcon({node}){221 return (<>{node.isOpen ? <ExpandMoreIcon /> : <ChevronRightIcon />}</>);222}223 224ToggleArrowIcon.propTypes = {225 node: PropTypes.object,226};227 228function checkAndSelectParent(chNode){229 let isAllChildSelected = true;230 chNode?.parent?.children?.forEach((child) => {231 if (!child.isSelected) {232 isAllChildSelected = false;233 }234 });235 if (chNode?.parent) {236 if (isAllChildSelected) {237 if (chNode.parent?.level == 0) {238 chNode.parent.data.isIndeterminate = true;239 } else {240 chNode.parent.data.isIndeterminate = false;241 }242 chNode.parent.selectMulti(chNode.parent.id);243 } else {244 chNode.parent.data.isIndeterminate = true;245 chNode.parent.selectMulti(chNode.parent.id);246 }247 chNode.parent.data.isSelected = true;248 checkAndSelectParent(chNode.parent);249 }250}251 252checkAndSelectParent.propTypes = {253 chNode: PropTypes.object254};255 256function delectPrentNode(chNode){257 if (chNode) {258 let isAnyChildSelected = false;259 chNode.children.forEach((childNode)=>{260 if(childNode.isSelected && !isAnyChildSelected){261 isAnyChildSelected = true;262 }263 });264 if(isAnyChildSelected){265 chNode.data.isSelected = true;266 chNode.data.isIndeterminate = true;267 } else {268 chNode.deselect(chNode);269 chNode.data.isSelected = false;270 }271 }272 273 if (chNode?.parent) {274 delectPrentNode(chNode.parent);275 }276}277 278function selectAllChild(chNode, tree, source, selectedNodeIds){279 let selectedChild = 0;280 chNode?.children?.forEach(child => {281 282 if(!child.isLeaf) {283 child.data.isIndeterminate = false;284 }285 if((source == 'expand' && selectedNodeIds.includes(child.id)) || source == 'checkbox') {286 child.data.isSelected = true;287 selectedChild += 1;288 }289 child.selectMulti(child.id);290 291 if (child?.children) {292 selectAllChild(child, tree, source, selectedNodeIds);293 }294 });295 296 if(selectedChild < chNode?.children.length ){297 chNode.data.isIndeterminate = true;298 } else {299 chNode.data.isIndeterminate = false;300 }301 302 if (chNode?.parent) {303 checkAndSelectParent(chNode);304 }305}306 307function deselectAllChild(chNode){308 chNode?.children.forEach(child => {309 child.deselect(child);310 child.data.isSelected = false;311 if (child?.children) {312 deselectAllChild(child);313 }314 });315}316 