sozsoft-platform/ui/src/views/menu/SortableMenuTree.tsx

220 lines
6.4 KiB
TypeScript
Raw Normal View History

import React, { useCallback, useEffect, useMemo, useState } from 'react'
2026-02-24 20:44:16 +00:00
import {
DndContext,
DragOverlay,
DragEndEvent,
DragStartEvent,
2026-02-24 20:44:16 +00:00
KeyboardSensor,
PointerSensor,
closestCenter,
2026-02-24 20:44:16 +00:00
useSensor,
useSensors,
} from '@dnd-kit/core'
import {
SortableContext,
sortableKeyboardCoordinates,
useSortable,
2026-02-24 20:44:16 +00:00
verticalListSortingStrategy,
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { MenuItemComponent, MenuItemComponentProps } from './MenuItemComponent'
import { findMenuItem, indentMenuItem, moveMenuItem, outdentMenuItem } from './menuTree.utils'
2026-02-24 20:44:16 +00:00
import { MenuItem } from '@/proxy/menus/menu'
import { PermissionDefinitionRecord } from '@/proxy/admin/models'
import { getPermissionsList } from '@/services/identity.service'
2026-02-24 20:44:16 +00:00
import { SelectBoxOption } from '@/types/shared'
interface SortableMenuTreeProps {
/** Ekranda gösterilen (filtrelenmiş olabilir) ağaç */
2026-02-24 20:44:16 +00:00
items: MenuItem[]
/** Taşıma işlemlerinin uygulanacağı tam ağaç */
sourceItems: MenuItem[]
2026-02-24 20:44:16 +00:00
onItemsChange: (items: MenuItem[]) => void
isDesignMode: boolean
isFiltered?: boolean
matchedIds?: Set<string>
collapsedIds: Set<string>
onToggleExpand: (id: string) => void
2026-02-24 20:44:16 +00:00
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 }}
/>
)
2026-02-24 20:44:16 +00:00
}
export const SortableMenuTree: React.FC<SortableMenuTreeProps> = ({
items,
sourceItems,
2026-02-24 20:44:16 +00:00
onItemsChange,
isDesignMode,
isFiltered = false,
matchedIds,
collapsedIds,
onToggleExpand,
2026-02-24 20:44:16 +00:00
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
2026-02-24 20:44:16 +00:00
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
2026-02-24 20:44:16 +00:00
)
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
2026-02-24 20:44:16 +00:00
}
}, [])
2026-02-24 20:44:16 +00:00
const handleDragStart = useCallback(
(event: DragStartEvent) => setActiveItem(findMenuItem(sourceItems, event.active.id as string)),
[sourceItems],
)
2026-02-24 20:44:16 +00:00
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
setActiveItem(null)
2026-02-24 20:44:16 +00:00
const { active, over } = event
if (!over || !dragEnabled || active.id === over.id) return
2026-02-24 20:44:16 +00:00
const next = moveMenuItem(sourceItems, active.id as string, over.id as string)
if (next) onItemsChange(next)
},
[dragEnabled, onItemsChange, sourceItems],
)
2026-02-24 20:44:16 +00:00
const handleIndent = useCallback(
(id: string) => {
const next = indentMenuItem(sourceItems, id)
if (next) onItemsChange(next)
},
[onItemsChange, sourceItems],
)
2026-02-24 20:44:16 +00:00
const handleOutdent = useCallback(
(id: string) => {
const next = outdentMenuItem(sourceItems, id)
if (next) onItemsChange(next)
},
[onItemsChange, sourceItems],
)
2026-02-24 20:44:16 +00:00
const renderMenuItem = (item: MenuItem, depth: number, index: number) => {
const id = item.id ?? ''
const children = item.children ?? []
const isExpanded = isFiltered || !collapsedIds.has(id)
2026-02-24 20:44:16 +00:00
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 && (
2026-02-24 20:44:16 +00:00
<SortableContext
items={children.map((child) => child.id ?? '').filter(Boolean)}
2026-02-24 20:44:16 +00:00
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),
)}
2026-02-24 20:44:16 +00:00
</div>
</SortableContext>
)}
</SortableMenuRow>
2026-02-24 20:44:16 +00:00
</div>
)
}
const rootIds = useMemo(
() => items.map((item) => item.id ?? '').filter(Boolean),
[items],
)
2026-02-24 20:44:16 +00:00
return (
<DndContext
collisionDetection={closestCenter}
sensors={sensors}
2026-02-24 20:44:16 +00:00
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={() => setActiveItem(null)}
2026-02-24 20:44:16 +00:00
>
<SortableContext items={rootIds} strategy={verticalListSortingStrategy}>
<div className="space-y-1">
{items.map((item, index) => renderMenuItem(item, 0, index))}
</div>
</SortableContext>
2026-02-24 20:44:16 +00:00
<DragOverlay>
{activeItem && (
2026-05-19 20:22:25 +00:00
<div className="bg-white dark:bg-gray-800 rounded-md shadow-lg">
<MenuItemComponent
isDragOverlay
2026-05-19 20:22:25 +00:00
depth={0}
isDesignMode={isDesignMode}
item={activeItem}
2026-05-19 20:22:25 +00:00
permissions={permissions}
refetch={refetch}
2026-05-19 20:22:25 +00:00
/>
</div>
)}
2026-02-24 20:44:16 +00:00
</DragOverlay>
</DndContext>
)
}