/* * Copyright 2021-2024 Avaiga Private Limited * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. */ import React, { useState, useCallback, useEffect, useMemo, SyntheticEvent, HTMLAttributes, forwardRef, Ref, CSSProperties, } from "react"; import Box from "@mui/material/Box"; import { SimpleTreeView as MuiTreeView } from "@mui/x-tree-view/SimpleTreeView"; import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import { TreeItem, TreeItemContentProps, useTreeItemState, TreeItemProps } from "@mui/x-tree-view/TreeItem"; import Paper from "@mui/material/Paper"; import TextField from "@mui/material/TextField"; import Tooltip from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; import { createSendUpdateAction } from "../../context/taipyReducers"; import { isLovParent, LovImage, paperBaseSx, SelTreeProps, showItem, useLovListMemo } from "./lovUtils"; import { useClassNames, useDispatch, useDispatchRequestUpdateOnFirstRender, useDynamicProperty, useModule, } from "../../utils/hooks"; import { LovItem } from "../../utils/lov"; import { getUpdateVar } from "./utils"; import { Icon } from "../../utils/icon"; const treeSlots = { expandIcon: ChevronRightIcon }; const CustomContent = forwardRef(function CustomContent(props: TreeItemContentProps, ref) { // need a display name const { classes, className, label, itemId, icon: iconProp, expansionIcon, displayIcon } = props; const { allowSelection, lovIcon, height } = props as unknown as CustomTreeProps; const { disabled, expanded, selected, focused, handleExpansion, handleSelection, preventSelection } = useTreeItemState(itemId); const icon = iconProp || expansionIcon || displayIcon; const classNames = [className, classes.root]; if (expanded) { classNames.push(classes.expanded); } if (selected) { classNames.push(classes.selected); } if (allowSelection && focused) { classNames.push(classes.focused); } if (disabled) { classNames.push(classes.disabled); } const divStyle = useMemo(() => (height ? { height: height } : undefined), [height]); return (
} style={divStyle} >
{icon}
{lovIcon ? : label}
); }); interface CustomTreeProps extends HTMLAttributes { allowSelection: boolean; lovIcon?: Icon; height?: string; } const CustomTreeItem = (props: TreeItemProps & CustomTreeProps) => { const { allowSelection, lovIcon, height, ...tiProps } = props; const ctProps = { allowSelection, lovIcon, height } as CustomTreeProps; return ; }; const renderTree = ( lov: LovItem[], active: boolean, searchValue: string, selectLeafsOnly: boolean, rowHeight?: string ) => { return lov.map((li) => { const children = li.children ? renderTree(li.children, active, searchValue, selectLeafsOnly, rowHeight) : []; if (!children.filter((c) => c).length && !showItem(li, searchValue)) { return null; } return ( {children} ); }); }; const boxSx = { width: "100%" } as CSSProperties; const textFieldSx = { mb: 1, px: 1, display: "flex" }; interface TreeViewProps extends SelTreeProps { defaultExpanded?: string | boolean; expanded?: string[] | boolean; selectLeafsOnly?: boolean; rowHeight?: string; } const TreeView = (props: TreeViewProps) => { const { id, defaultValue = "", value, updateVarName = "", defaultLov = "", filter = false, multiple = false, propagate = true, lov, updateVars = "", width = "100%", height, valueById, selectLeafsOnly = false, rowHeight, } = props; const [searchValue, setSearchValue] = useState(""); const [selectedValue, setSelectedValue] = useState([]); const [oneExpanded, setOneExpanded] = useState(false); const [refreshExpanded, setRefreshExpanded] = useState(false); const [expandedNodes, setExpandedNodes] = useState([]); const dispatch = useDispatch(); const module = useModule(); const className = useClassNames(props.libClassName, props.dynamicClassName, props.className); const active = useDynamicProperty(props.active, props.defaultActive, true); const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined); useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars, updateVarName); const lovList = useLovListMemo(lov, defaultLov, true); const treeSx = useMemo( () => ({ bgcolor: "transparent", overflowY: "auto", width: "100%", maxWidth: width }), [width] ); const paperSx = useMemo(() => { const sx = height === undefined ? paperBaseSx : { ...paperBaseSx, maxHeight: height }; return { ...sx, overflow: "hidden", py: 1 }; }, [height]); useEffect(() => { let refExp = false; let oneExp = false; if (props.expanded === undefined) { if (typeof props.defaultExpanded === "boolean") { oneExp = !props.defaultExpanded; } else if (typeof props.defaultExpanded === "string") { try { const val = JSON.parse(props.defaultExpanded); if (Array.isArray(val)) { setExpandedNodes(val.map((v) => "" + v)); } else { setExpandedNodes(["" + val]); } refExp = true; } catch (e) { console.info(`Tree.expanded cannot parse property\n${(e as Error).message || e}`); } } } else if (typeof props.expanded === "boolean") { oneExp = !props.expanded; } else { try { if (Array.isArray(props.expanded)) { setExpandedNodes(props.expanded.map((v) => "" + v)); } else { setExpandedNodes(["" + props.expanded]); } refExp = true; } catch (e) { console.info(`Tree.expanded wrongly formatted property\n${(e as Error).message || e}`); } } setOneExpanded(oneExp); setRefreshExpanded(refExp); }, [props.defaultExpanded, props.expanded]); useEffect(() => { if (value !== undefined) { setSelectedValue(Array.isArray(value) ? value : [value]); } else if (defaultValue) { let parsedValue; try { parsedValue = JSON.parse(defaultValue); } catch (e) { parsedValue = defaultValue; } setSelectedValue(Array.isArray(parsedValue) ? parsedValue : [parsedValue]); } }, [defaultValue, value, multiple]); const clickHandler = useCallback( (event: SyntheticEvent, nodeIds: string[] | string | null) => { const ids = nodeIds === null ? [] : Array.isArray(nodeIds) ? nodeIds : [nodeIds]; setSelectedValue(ids); updateVarName && dispatch( createSendUpdateAction( updateVarName, ids, module, props.onChange, propagate, valueById ? undefined : getUpdateVar(updateVars, "lov") ) ); }, [updateVarName, dispatch, propagate, updateVars, valueById, props.onChange, module] ); const handleInput = useCallback((e: React.ChangeEvent) => setSearchValue(e.target.value), []); const handleNodeToggle = useCallback( (event: React.SyntheticEvent, nodeIds: string[]) => { const expVar = getUpdateVar(updateVars, "expanded"); if (oneExpanded) { setExpandedNodes((en) => { if (en.length < nodeIds.length) { // node opened: keep only parent nodes nodeIds = nodeIds.filter((n, i) => i == 0 || isLovParent(lovList, n, nodeIds[0])); } if (refreshExpanded) { dispatch(createSendUpdateAction(expVar, nodeIds, module, props.onChange, propagate)); } return nodeIds; }); } else { setExpandedNodes(nodeIds); if (refreshExpanded) { dispatch(createSendUpdateAction(expVar, nodeIds, module, props.onChange, propagate)); } } }, [oneExpanded, refreshExpanded, lovList, propagate, updateVars, dispatch, props.onChange, module] ); const treeProps = useMemo(() => ({ multiSelect: multiple, selectedItems: selectedValue }), [multiple, selectedValue]); return ( {filter && ( )} {renderTree(lovList, !!active, searchValue, selectLeafsOnly, rowHeight)} ); }; export default TreeView;