sozsoft-platform/ui/src/views/menu/SortableMenuTree.tsx
2026-08-11 13:29:41 +03:00

219 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
)
}