237 lines
6.4 KiB
TypeScript
237 lines
6.4 KiB
TypeScript
import React, { useEffect, useState } from 'react'
|
|
import {
|
|
DndContext,
|
|
DragOverlay,
|
|
closestCenter,
|
|
KeyboardSensor,
|
|
PointerSensor,
|
|
useSensor,
|
|
useSensors,
|
|
DragStartEvent,
|
|
DragEndEvent,
|
|
} from '@dnd-kit/core'
|
|
import {
|
|
SortableContext,
|
|
sortableKeyboardCoordinates,
|
|
verticalListSortingStrategy,
|
|
} from '@dnd-kit/sortable'
|
|
import { MenuItemComponent } from './MenuItemComponent'
|
|
import { MenuItem } from '@/@types/menu'
|
|
import { getPermissionsList } from '@/services/identity.service'
|
|
import { PermissionDefinitionRecord } from '@/proxy/admin/models'
|
|
import { SelectBoxOption } from '@/shared/types'
|
|
|
|
interface SortableMenuTreeProps {
|
|
items: MenuItem[]
|
|
onItemsChange: (items: MenuItem[]) => void
|
|
isDesignMode: boolean
|
|
refetch: () => void
|
|
}
|
|
|
|
export const SortableMenuTree: React.FC<SortableMenuTreeProps> = ({
|
|
items,
|
|
onItemsChange,
|
|
isDesignMode,
|
|
refetch,
|
|
}) => {
|
|
const [permissions, setPermissions] = useState<SelectBoxOption[]>([])
|
|
const [activeItem, setActiveItem] = React.useState<MenuItem | null>(null)
|
|
|
|
const sensors = useSensors(
|
|
useSensor(PointerSensor, {
|
|
activationConstraint: {
|
|
distance: 8,
|
|
},
|
|
}),
|
|
useSensor(KeyboardSensor, {
|
|
coordinateGetter: sortableKeyboardCoordinates,
|
|
}),
|
|
)
|
|
|
|
const flattenItems = (
|
|
items: MenuItem[],
|
|
parentPath: number[] = [],
|
|
): Array<{ item: MenuItem; path: number[] }> => {
|
|
return items.flatMap((item, index) => {
|
|
const path = [...parentPath, index]
|
|
const self = { item, path }
|
|
|
|
if (item.children?.length) {
|
|
return [self, ...flattenItems(item.children, path)]
|
|
}
|
|
|
|
return [self]
|
|
})
|
|
}
|
|
|
|
const findItemById = (items: MenuItem[], id: string): MenuItem | null => {
|
|
for (const item of items) {
|
|
if (item.id === id) return item
|
|
if (item.children) {
|
|
const found = findItemById(item.children, id)
|
|
if (found) return found
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
const removeItemFromTree = (items: MenuItem[], id: string): MenuItem[] => {
|
|
return items.reduce((acc: MenuItem[], item) => {
|
|
if (item.id === id) return acc
|
|
|
|
const newItem = { ...item }
|
|
if (newItem.children && newItem.children.length > 0) {
|
|
newItem.children = removeItemFromTree(newItem.children, id)
|
|
}
|
|
|
|
acc.push(newItem)
|
|
return acc
|
|
}, [])
|
|
}
|
|
|
|
const insertItemAtPath = (
|
|
items: MenuItem[],
|
|
item: MenuItem,
|
|
targetPath: number[],
|
|
): MenuItem[] => {
|
|
if (targetPath.length === 1) {
|
|
const newItems = [...items]
|
|
newItems.splice(targetPath[0], 0, item)
|
|
return newItems
|
|
}
|
|
|
|
const [firstIndex, ...restPath] = targetPath
|
|
const newItems = [...items]
|
|
|
|
if (newItems[firstIndex]) {
|
|
newItems[firstIndex] = {
|
|
...newItems[firstIndex],
|
|
children: insertItemAtPath(newItems[firstIndex].children || [], item, restPath),
|
|
}
|
|
}
|
|
|
|
return newItems
|
|
}
|
|
|
|
const getInsertionPath = (
|
|
items: MenuItem[],
|
|
activeId: string,
|
|
overId: string,
|
|
): number[] | null => {
|
|
const flat = flattenItems(items)
|
|
const activeFlat = flat.find((f) => f.item.id === activeId)
|
|
const overFlat = flat.find((f) => f.item.id === overId)
|
|
|
|
if (!activeFlat || !overFlat) return null
|
|
|
|
const isSameParent = activeFlat.path.slice(0, -1).join() === overFlat.path.slice(0, -1).join()
|
|
|
|
const insertPath = [...overFlat.path]
|
|
|
|
if (isSameParent) {
|
|
const activeIndex = activeFlat.path[activeFlat.path.length - 2]
|
|
const overIndex = overFlat.path[overFlat.path.length - 1]
|
|
|
|
if (activeIndex < overIndex) {
|
|
insertPath[insertPath.length - 1] += 1
|
|
}
|
|
}
|
|
|
|
return insertPath
|
|
}
|
|
|
|
const updateOrderNumbers = (items: MenuItem[]): MenuItem[] => {
|
|
return items.map((item, index) => ({
|
|
...item,
|
|
order: index + 1,
|
|
children: item.children ? updateOrderNumbers(item.children) : [],
|
|
}))
|
|
}
|
|
|
|
const handleDragStart = (event: DragStartEvent) => {
|
|
const { active } = event
|
|
const activeItem = findItemById(items, active.id as string)
|
|
setActiveItem(activeItem)
|
|
}
|
|
|
|
const handleDragEnd = (event: DragEndEvent) => {
|
|
const { active, over } = event
|
|
setActiveItem(null)
|
|
|
|
if (!over || active.id === over.id || !isDesignMode) return
|
|
|
|
const activeId = active.id as string
|
|
const overId = over.id as string
|
|
const activeItem = findItemById(items, activeId)
|
|
if (!activeItem) return
|
|
|
|
const insertionPath = getInsertionPath(items, activeId, overId)
|
|
if (!insertionPath) return
|
|
|
|
let newItems = removeItemFromTree(items, activeId)
|
|
newItems = insertItemAtPath(newItems, activeItem, insertionPath)
|
|
const finalItems = updateOrderNumbers(newItems)
|
|
onItemsChange(finalItems)
|
|
}
|
|
|
|
useEffect(() => {
|
|
const fetchPermissions = async () => {
|
|
const response = await getPermissionsList()
|
|
if (response.data) {
|
|
setPermissions(
|
|
response.data.map((p: PermissionDefinitionRecord) => ({
|
|
value: p.name,
|
|
label: p.name,
|
|
})),
|
|
)
|
|
}
|
|
}
|
|
|
|
fetchPermissions()
|
|
}, [])
|
|
|
|
const renderMenuItem = (item: MenuItem, depth: number = 0): React.ReactNode => {
|
|
return (
|
|
<div key={item.id}>
|
|
<MenuItemComponent item={item} isDesignMode={isDesignMode} depth={depth} refetch={refetch} permissions={permissions}>
|
|
{Array.isArray(item.children) && item.children.length > 0 && (
|
|
<SortableContext
|
|
items={item.children
|
|
.filter((child): child is MenuItem & { id: string } => !!child.id)
|
|
.map((child) => child.id)}
|
|
strategy={verticalListSortingStrategy}
|
|
>
|
|
<div className="ml-4">
|
|
{item.children.map((child) => renderMenuItem(child, depth + 1))}
|
|
</div>
|
|
</SortableContext>
|
|
)}
|
|
</MenuItemComponent>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<DndContext
|
|
sensors={sensors}
|
|
collisionDetection={closestCenter}
|
|
onDragStart={handleDragStart}
|
|
onDragEnd={handleDragEnd}
|
|
>
|
|
<div className="space-y-1">{items.map((item) => renderMenuItem(item))}</div>
|
|
|
|
<DragOverlay>
|
|
{activeItem ? (
|
|
<MenuItemComponent
|
|
item={activeItem}
|
|
isDesignMode={isDesignMode}
|
|
depth={0}
|
|
isDragOverlay={true}
|
|
refetch={refetch}
|
|
permissions={permissions}
|
|
/>
|
|
) : null}
|
|
</DragOverlay>
|
|
</DndContext>
|
|
)
|
|
}
|