219 lines
6.4 KiB
TypeScript
219 lines
6.4 KiB
TypeScript
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||
import {
|
||
DndContext,
|
||
DragOverlay,
|
||
DragEndEvent,
|
||
DragStartEvent,
|
||
KeyboardSensor,
|
||
PointerSensor,
|
||
closestCenter,
|
||
useSensor,
|
||
useSensors,
|
||
} from '@dnd-kit/core'
|
||
import {
|
||
SortableContext,
|
||
sortableKeyboardCoordinates,
|
||
useSortable,
|
||
verticalListSortingStrategy,
|
||
} from '@dnd-kit/sortable'
|
||
import { CSS } from '@dnd-kit/utilities'
|
||
import { MenuItemComponent, MenuItemComponentProps } from './MenuItemComponent'
|
||
import { findMenuItem, indentMenuItem, moveMenuItem, outdentMenuItem } from './menuTree.utils'
|
||
import { MenuItem } from '@/proxy/menus/menu'
|
||
import { PermissionDefinitionRecord } from '@/proxy/admin/models'
|
||
import { getPermissionsList } from '@/services/identity.service'
|
||
import { SelectBoxOption } from '@/types/shared'
|
||
|
||
interface SortableMenuTreeProps {
|
||
/** Ekranda gösterilen (filtrelenmiş olabilir) ağaç */
|
||
items: MenuItem[]
|
||
/** Taşıma işlemlerinin uygulanacağı tam ağaç */
|
||
sourceItems: MenuItem[]
|
||
onItemsChange: (items: MenuItem[]) => void
|
||
isDesignMode: boolean
|
||
isFiltered?: boolean
|
||
matchedIds?: Set<string>
|
||
collapsedIds: Set<string>
|
||
onToggleExpand: (id: string) => void
|
||
refetch: () => void
|
||
}
|
||
|
||
type SortableRowProps = Omit<MenuItemComponentProps, 'setNodeRef' | 'dragHandleProps' | 'style'> & {
|
||
disabled: boolean
|
||
}
|
||
|
||
const SortableMenuRow: React.FC<SortableRowProps> = ({ disabled, ...props }) => {
|
||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||
id: props.item.id ?? '',
|
||
data: { type: 'menu-item' },
|
||
disabled,
|
||
})
|
||
|
||
return (
|
||
<MenuItemComponent
|
||
{...props}
|
||
isDragging={isDragging}
|
||
setNodeRef={setNodeRef}
|
||
dragHandleProps={disabled ? undefined : { ...attributes, ...listeners }}
|
||
style={{ transform: CSS.Transform.toString(transform), transition }}
|
||
/>
|
||
)
|
||
}
|
||
|
||
export const SortableMenuTree: React.FC<SortableMenuTreeProps> = ({
|
||
items,
|
||
sourceItems,
|
||
onItemsChange,
|
||
isDesignMode,
|
||
isFiltered = false,
|
||
matchedIds,
|
||
collapsedIds,
|
||
onToggleExpand,
|
||
refetch,
|
||
}) => {
|
||
const [permissions, setPermissions] = useState<SelectBoxOption[]>([])
|
||
const [activeItem, setActiveItem] = useState<MenuItem | null>(null)
|
||
|
||
// Filtre açıkken ara düğümler gizli olduğu için sürükle-bırak kapatılır.
|
||
const dragEnabled = isDesignMode && !isFiltered
|
||
|
||
const sensors = useSensors(
|
||
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
|
||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
||
)
|
||
|
||
useEffect(() => {
|
||
let cancelled = false
|
||
|
||
getPermissionsList()
|
||
.then((response) => {
|
||
if (cancelled || !response.data) return
|
||
|
||
const options = response.data
|
||
.filter((permission: PermissionDefinitionRecord) => Boolean(permission.name))
|
||
.map((permission: PermissionDefinitionRecord) => ({
|
||
value: permission.name,
|
||
label: permission.name,
|
||
}))
|
||
.sort((a, b) => (a.label ?? '').localeCompare(b.label ?? ''))
|
||
|
||
setPermissions(options)
|
||
})
|
||
.catch(() => {
|
||
if (!cancelled) setPermissions([])
|
||
})
|
||
|
||
return () => {
|
||
cancelled = true
|
||
}
|
||
}, [])
|
||
|
||
const handleDragStart = useCallback(
|
||
(event: DragStartEvent) => setActiveItem(findMenuItem(sourceItems, event.active.id as string)),
|
||
[sourceItems],
|
||
)
|
||
|
||
const handleDragEnd = useCallback(
|
||
(event: DragEndEvent) => {
|
||
setActiveItem(null)
|
||
|
||
const { active, over } = event
|
||
if (!over || !dragEnabled || active.id === over.id) return
|
||
|
||
const next = moveMenuItem(sourceItems, active.id as string, over.id as string)
|
||
if (next) onItemsChange(next)
|
||
},
|
||
[dragEnabled, onItemsChange, sourceItems],
|
||
)
|
||
|
||
const handleIndent = useCallback(
|
||
(id: string) => {
|
||
const next = indentMenuItem(sourceItems, id)
|
||
if (next) onItemsChange(next)
|
||
},
|
||
[onItemsChange, sourceItems],
|
||
)
|
||
|
||
const handleOutdent = useCallback(
|
||
(id: string) => {
|
||
const next = outdentMenuItem(sourceItems, id)
|
||
if (next) onItemsChange(next)
|
||
},
|
||
[onItemsChange, sourceItems],
|
||
)
|
||
|
||
const renderMenuItem = (item: MenuItem, depth: number, index: number) => {
|
||
const id = item.id ?? ''
|
||
const children = item.children ?? []
|
||
const isExpanded = isFiltered || !collapsedIds.has(id)
|
||
|
||
return (
|
||
<div key={id || `${depth}-${index}`} className="rounded-md">
|
||
<SortableMenuRow
|
||
canIndent={isDesignMode && !isFiltered && index > 0}
|
||
canOutdent={isDesignMode && !isFiltered && depth > 0}
|
||
depth={depth}
|
||
disabled={!dragEnabled}
|
||
isDesignMode={isDesignMode}
|
||
isExpanded={isExpanded}
|
||
isHighlighted={Boolean(matchedIds?.has(id))}
|
||
item={item}
|
||
permissions={permissions}
|
||
refetch={refetch}
|
||
onIndent={handleIndent}
|
||
onOutdent={handleOutdent}
|
||
onToggleExpand={onToggleExpand}
|
||
>
|
||
{children.length > 0 && (
|
||
<SortableContext
|
||
items={children.map((child) => child.id ?? '').filter(Boolean)}
|
||
strategy={verticalListSortingStrategy}
|
||
>
|
||
<div className="ml-4 border-l border-gray-200 dark:border-gray-700 pl-1">
|
||
{children.map((child, childIndex) =>
|
||
renderMenuItem(child, depth + 1, childIndex),
|
||
)}
|
||
</div>
|
||
</SortableContext>
|
||
)}
|
||
</SortableMenuRow>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const rootIds = useMemo(
|
||
() => items.map((item) => item.id ?? '').filter(Boolean),
|
||
[items],
|
||
)
|
||
|
||
return (
|
||
<DndContext
|
||
collisionDetection={closestCenter}
|
||
sensors={sensors}
|
||
onDragStart={handleDragStart}
|
||
onDragEnd={handleDragEnd}
|
||
onDragCancel={() => setActiveItem(null)}
|
||
>
|
||
<SortableContext items={rootIds} strategy={verticalListSortingStrategy}>
|
||
<div className="space-y-1">
|
||
{items.map((item, index) => renderMenuItem(item, 0, index))}
|
||
</div>
|
||
</SortableContext>
|
||
|
||
<DragOverlay>
|
||
{activeItem && (
|
||
<div className="bg-white dark:bg-gray-800 rounded-md shadow-lg">
|
||
<MenuItemComponent
|
||
isDragOverlay
|
||
depth={0}
|
||
isDesignMode={isDesignMode}
|
||
item={activeItem}
|
||
permissions={permissions}
|
||
refetch={refetch}
|
||
/>
|
||
</div>
|
||
)}
|
||
</DragOverlay>
|
||
</DndContext>
|
||
)
|
||
}
|