import { DragLayerMonitorProps, DropOptions, MultiBackend, NodeModel, PlaceholderRenderParams, RenderParams, Tree, TreeProps, getBackendOptions } from "@minoru/react-dnd-treeview" import { Button, Chip, ListItemIcon, ListItemText, Menu, MenuItem, alpha, useTheme } from '@mui/material' import { MD5 } from 'crypto-js' import * as React from 'react' import { DndProvider } from "react-dnd" import { FaTrash } from "react-icons/fa" import { IoIosAdd } from "react-icons/io" import { MdOutlineKeyboardArrowDown, MdOutlineKeyboardArrowRight } from "react-icons/md" import { TbEdit } from "react-icons/tb" export const TreeItemChipComponent = (props: { node: NodeModelExt, }) => { const { node } = props if (node.state == 'new') return if (node.state == 'updated') return if (node.state == 'deleted') return return null } export type NodeModelExt = NodeModel & { state: 'none' | 'new' | 'updated' | 'deleted', position: number, } export const TreeComponent = (props: { tree: NodeModelExt[], rootId?: string | number, render?: (node: NodeModelExt, params: RenderParams) => React.ReactElement, onDrop: (tree: NodeModelExt[], options: DropOptions) => void, persistId?: string, // persiste apenas o estado aberto|fechado, nao os dados onItemEditClick?: (node: NodeModelExt) => void, onItemAddChildrenClick?: (parentNode: NodeModel, childPosition: number) => boolean, onItemRemoveClick?: (node: NodeModelExt) => void, renderItemContent?: (node: NodeModel, params: RenderParams) => React.ReactElement, getText?: (node: NodeModel) => string, } & Omit, 'tree' | 'rootId' | 'render' | 'onDrop'>) => { const { tree, rootId, render, onDrop, persistId, onItemEditClick, onItemAddChildrenClick, onItemRemoveClick, renderItemContent, getText, ...rest } = props const theme = useTheme() const [menuAnchor, setAnchorElemnt] = React.useState Element) | null | undefined>(null) const [menuNodeItem, setMenuNodeItem] = React.useState | undefined | null>(null) const [openNodes, setOpenNodes] = React.useState<(string | number)[]>(() => { if (persistId) { const value = localStorage.getItem(persistId); return value ? JSON.parse(value) : []; } else { return [] } }); const openMenu = Boolean(menuAnchor) const ident = 20 const highLightColor = '#2196f3' const itemDefaultStyle: React.CSSProperties = { boxSizing: 'border-box', borderRadius: 5, } const itemHighlightedStyle: React.CSSProperties = { borderWidth: 1, borderStyle: 'solid', borderColor: alpha(theme.palette.divider, .5), background: alpha(theme.palette.divider, .5), } const handleRender: (node: NodeModelExt, params: RenderParams) => React.ReactElement = (node, params) => { const { isDropTarget, depth, isOpen, onToggle } = params const dropTargetStyle: React.CSSProperties | undefined = isDropTarget ? { color: highLightColor, } : undefined const hasChildren = tree.some(n => n.parent == node.id) const buttonGap = 0 const defaultButtonStyle: React.CSSProperties = { display: 'flex', justifyContent: 'start', textTransform: 'none', gap: buttonGap, color: theme.palette.text.primary, ...dropTargetStyle, } const content = <>{renderItemContent?.(node, params) ?? (getText?.(node) ?? node.text)} return
{hasChildren ? : }
} const dragPreviewRender: (monitorProps: DragLayerMonitorProps) => React.ReactElement = (monitorProps) => ( {monitorProps.item.text} ) const placeholderRender: (node: NodeModel, params: PlaceholderRenderParams) => React.ReactElement = (_node, { depth: _depth }) => ( <>
) const key = MD5(JSON.stringify(tree)).toString() return <> setAnchorElemnt(null)} slotProps={{ list: { sx: { p: 0, } }, paper: { sx: { // marginTop: .5, // minWidth: 260, borderRadius: 3, boxShadow: "0 8px 24px rgba(0,0,0,0.12)", overflow: "hidden", }, } }} > { setAnchorElemnt(null) if (menuNodeItem) onItemEditClick?.(menuNodeItem) }} > { setAnchorElemnt(null) if (menuNodeItem) { const menuNodeItemChildrenLen = tree.filter(c => c.parent == menuNodeItem.id).length const newChildPosition = menuNodeItemChildrenLen + 1 if (onItemAddChildrenClick?.(menuNodeItem, newChildPosition)) { setOpenNodes(old => { const result = [...old, menuNodeItem.id] return result }) } } }} > { setAnchorElemnt(null) if (menuNodeItem) onItemRemoveClick?.(menuNodeItem) }} > [], options: DropOptions) => { // https://claude.ai/share/4339c7aa-6280-43f6-a883-fdc22b45393c // 1. Mapa auxiliar: novo master_uuid (parent) de cada nó, baseado na estrutura pós-drop const newParentById = new Map() newTree.forEach(n => { newParentById.set(String(n.id), n.parent ? String(n.parent) : "") }) // 2. Contador de ordem por nível, usando o NOVO parent como chave const orderCounters = new Map() const repositionedTree = newTree.map(newTreeNode => { const newMasterUuid = newParentById.get(String(newTreeNode.id)) ?? "" const nextOrder = (orderCounters.get(newMasterUuid) ?? 0) + 1 orderCounters.set(newMasterUuid, nextOrder) return { ...newTreeNode as NodeModelExt, position: nextOrder, } as NodeModelExt }); // 3. Compara contra a árvore ANTERIOR (tree, via props) para detectar // todos os itens cujo parent ou position mudaram const oldByUuid = new Map(tree.map(d => [d.id, d])) const updatedItems = repositionedTree.filter(novo => { const antigo = oldByUuid.get(novo.id) if (!antigo) return false // não deveria acontecer aqui, mas por segurança return antigo.parent != novo.parent || antigo.position != novo.position }) // 4. Marca como 'updated' os itens alterados, preservando 'new' se já era novo const updatedStateTree = repositionedTree.map(n => { const updated = updatedItems.some(i => i.id == n.id) if (!updated) return n return { ...n, state: n.state == 'new' ? 'new' : 'updated', } as NodeModelExt }) onDrop(updatedStateTree, options) }} // nullable dropTargetOffset={props.dropTargetOffset ?? 5} sort={props.sort != null ? props.sort : false} listComponent={props.listComponent ?? "div"} listItemComponent={props.listItemComponent ?? "div"} placeholderComponent={props.placeholderComponent ?? "div"} enableAnimateExpand={props.enableAnimateExpand != null ? props.enableAnimateExpand : true} initialOpen={props.initialOpen ?? openNodes} onChangeOpen={(ids) => { setOpenNodes(ids); if (persistId) localStorage.setItem(persistId, JSON.stringify(ids)); props.onChangeOpen?.(ids) }} dragPreviewRender={props.dragPreviewRender ?? dragPreviewRender} placeholderRender={props.placeholderRender ?? placeholderRender} /> }