React Tree Component with Smooth Expand/Collapse Animation

/**
 * @file
 * @author
 * @version
 * @since
 * @description This is a generic component used to show the cube's outline (dimensions roles tree).
 */
import * as React from 'react';
import Box from '@mui/material/Box';
import { useEffect, useState } from 'react';
import MetaApi from '../utils/meta-api';

const CubeOutline = ({ cubeGid, callback_selected_node }) => {

  const [treeData, setTreeData] = useState([]);
  const [selectedNodeKey, setSelectedNodeKey] = useState(null);

  useEffect(() => {

    const initializeData = async () => {
      if (cubeGid) {
        const dimensionRoles = await MetaApi.load_cube_dim_roles(cubeGid);
        const mappedData = dimensionRoles.map((role) => ({
          id: `${role.gid}`,
          status: '[ + ]',
          label: `[Dimension Role] ${role.name}`,
          nodeType: 'dimension_role',
          entity: role,
          subNodes: [],
        }));
        setTreeData(mappedData);
      }
    };

    initializeData();

  }, [cubeGid]);

  const updateNodeStatus = async (currentNode) => {
    currentNode.status = currentNode.status === '[ + ]' ? '[ - ]' : '[ + ]';
    
    if (currentNode.status === '[ - ]') {
      if (currentNode.nodeType === 'dimension_role') {
        const role = currentNode.entity;
        const hierarchies = await MetaApi.load_dim_hierarchies(role.dimensionGid);
        currentNode.subNodes = hierarchies.map((hierarchy) => ({
          id: `${role.gid}_${hierarchy.gid}`,
          status: '[ + ]',
          label: `[Hierarchy Role] ${hierarchy.name}`,
          nodeType: 'hierarchy_role',
          entity: {
            dimensionRole: role,
            hierarchy,
          },
          subNodes: [],
        }));
      } else if (currentNode.nodeType === 'hierarchy_role') {
        const dimRole = currentNode.entity.dimensionRole;
        const hierarchy = currentNode.entity.hierarchy;
        const members = await MetaApi.load_hierarchy_members(hierarchy.gid);
        const rootMember = members.filter((member) => member.parentGid === 0)[0];

        currentNode.subNodes = [{
          id: `${dimRole.gid}_${rootMember.gid}`,
          status: '[ + ]',
          label: `[Member Role] ${rootMember.name}`,
          nodeType: 'member_role',
          entity: {
            dimensionRole: dimRole,
            member: rootMember,
          },
          subNodes: [],
        }];
      } else if (currentNode.nodeType === 'member_role') {
        const parentMember = currentNode.entity.member;
        const hierarchyId = currentNode.entity.member.hierarchyGid;
        let childMembers = await MetaApi.load_hierarchy_members(hierarchyId);
        childMembers = childMembers.filter((member) => member.parentGid === parentMember.gid);

        currentNode.subNodes = childMembers.map((member) => ({
          id: `${currentNode.entity.dimensionRole.gid}_${member.gid}`,
          status: '[ + ]',
          label: `[Member Role] ${member.name}`,
          nodeType: 'member_role',
          entity: {
            dimensionRole: currentNode.entity.dimensionRole,
            member: member,
          },
          subNodes: [],
        }));
      }
    } else {
      currentNode.subNodes = [];
    }

    setTreeData([...treeData]);
  };

  const onNodeSelect = (node) => {
    setSelectedNodeKey(node.id);
    callback_selected_node(node);
  };

  const renderTreeNode = (node) => {
    const isNodeSelected = node.id === selectedNodeKey;
    return (
      <Box key={node.id} sx={{ paddingLeft: 2 }}>
        <Box sx={{ textAlign: 'left' }}>
          <Box>
            <span
              style={{ cursor: 'pointer' }}
              onClick={() => updateNodeStatus(node)}
            >
              {node.status}
            </span>
            <span
              style={{
                cursor: 'pointer',
                marginLeft: 8,
                backgroundColor: isNodeSelected ? '#d3d3d3' : 'transparent',
                padding: '2px 4px',
              }}
              onClick={() => onNodeSelect(node)}
            >
              {node.label}
            </span>
          </Box>
        </Box>
        
        {node.subNodes && node.subNodes.length > 0 && (
          <Box
            sx={{
              paddingLeft: 2,
              overflow: 'hidden',
              maxHeight: node.status === '[ - ]' ? '1000px' : '0px',
              transition: 'max-height 0.3s ease-in-out',
            }}
          >
            {node.subNodes.map(renderTreeNode)}
          </Box>
        )}
      </Box>
    );
  };

  return (
    <Box sx={{ minHeight: 352, minWidth: 250 }}>
      {treeData.map(renderTreeNode)}
    </Box>
  );
};

export default CubeOutline;


Tags: React MUI javascript animation tree-component

Posted on Tue, 16 Jun 2026 16:16:17 +0000 by visionmaster