2026-08-11 10:29:41 +00:00
|
|
|
|
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
import {
|
|
|
|
|
|
DndContext,
|
|
|
|
|
|
DragOverlay,
|
2026-08-11 10:29:41 +00:00
|
|
|
|
DragEndEvent,
|
|
|
|
|
|
DragStartEvent,
|
2026-02-24 20:44:16 +00:00
|
|
|
|
KeyboardSensor,
|
|
|
|
|
|
PointerSensor,
|
2026-08-11 10:29:41 +00:00
|
|
|
|
closestCenter,
|
2026-02-24 20:44:16 +00:00
|
|
|
|
useSensor,
|
|
|
|
|
|
useSensors,
|
|
|
|
|
|
} from '@dnd-kit/core'
|
|
|
|
|
|
import {
|
|
|
|
|
|
SortableContext,
|
|
|
|
|
|
sortableKeyboardCoordinates,
|
2026-08-11 10:29:41 +00:00
|
|
|
|
useSortable,
|
2026-02-24 20:44:16 +00:00
|
|
|
|
verticalListSortingStrategy,
|
|
|
|
|
|
} from '@dnd-kit/sortable'
|
2026-08-11 10:29:41 +00:00
|
|
|
|
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'
|
2026-08-11 10:29:41 +00:00
|
|
|
|
import { getPermissionsList } from '@/services/identity.service'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
import { SelectBoxOption } from '@/types/shared'
|
|
|
|
|
|
|
|
|
|
|
|
interface SortableMenuTreeProps {
|
2026-08-11 10:29:41 +00:00
|
|
|
|
/** Ekranda gösterilen (filtrelenmiş olabilir) ağaç */
|
2026-02-24 20:44:16 +00:00
|
|
|
|
items: MenuItem[]
|
2026-08-11 10:29:41 +00:00
|
|
|
|
/** Taşıma işlemlerinin uygulanacağı tam ağaç */
|
|
|
|
|
|
sourceItems: MenuItem[]
|
2026-02-24 20:44:16 +00:00
|
|
|
|
onItemsChange: (items: MenuItem[]) => void
|
|
|
|
|
|
isDesignMode: boolean
|
2026-08-11 10:29:41 +00:00
|
|
|
|
isFiltered?: boolean
|
|
|
|
|
|
matchedIds?: Set<string>
|
|
|
|
|
|
collapsedIds: Set<string>
|
|
|
|
|
|
onToggleExpand: (id: string) => void
|
2026-02-24 20:44:16 +00:00
|
|
|
|
refetch: () => void
|
2026-08-11 10:29:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
2026-08-11 10:29:41 +00:00
|
|
|
|
sourceItems,
|
2026-02-24 20:44:16 +00:00
|
|
|
|
onItemsChange,
|
|
|
|
|
|
isDesignMode,
|
2026-08-11 10:29:41 +00:00
|
|
|
|
isFiltered = false,
|
|
|
|
|
|
matchedIds,
|
|
|
|
|
|
collapsedIds,
|
|
|
|
|
|
onToggleExpand,
|
2026-02-24 20:44:16 +00:00
|
|
|
|
refetch,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
const [permissions, setPermissions] = useState<SelectBoxOption[]>([])
|
2026-08-11 10:29:41 +00:00
|
|
|
|
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(
|
2026-08-11 10:29:41 +00:00
|
|
|
|
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
|
|
|
|
|
|
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
2026-02-24 20:44:16 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-11 10:29:41 +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-08-11 10:29:41 +00:00
|
|
|
|
}, [])
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-08-11 10:29:41 +00:00
|
|
|
|
const handleDragStart = useCallback(
|
|
|
|
|
|
(event: DragStartEvent) => setActiveItem(findMenuItem(sourceItems, event.active.id as string)),
|
|
|
|
|
|
[sourceItems],
|
|
|
|
|
|
)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-08-11 10:29:41 +00:00
|
|
|
|
const handleDragEnd = useCallback(
|
|
|
|
|
|
(event: DragEndEvent) => {
|
|
|
|
|
|
setActiveItem(null)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-08-11 10:29:41 +00:00
|
|
|
|
const { active, over } = event
|
|
|
|
|
|
if (!over || !dragEnabled || active.id === over.id) return
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-08-11 10:29:41 +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
|
|
|
|
|
2026-08-11 10:29:41 +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
|
|
|
|
|
2026-08-11 10:29:41 +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
|
|
|
|
|
2026-08-11 10:29:41 +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 (
|
2026-08-11 10:29:41 +00:00
|
|
|
|
<div key={id || `${depth}-${index}`} className="rounded-md">
|
|
|
|
|
|
<SortableMenuRow
|
|
|
|
|
|
canIndent={isDesignMode && !isFiltered && index > 0}
|
|
|
|
|
|
canOutdent={isDesignMode && !isFiltered && depth > 0}
|
2026-07-13 13:51:13 +00:00
|
|
|
|
depth={depth}
|
2026-08-11 10:29:41 +00:00
|
|
|
|
disabled={!dragEnabled}
|
|
|
|
|
|
isDesignMode={isDesignMode}
|
|
|
|
|
|
isExpanded={isExpanded}
|
|
|
|
|
|
isHighlighted={Boolean(matchedIds?.has(id))}
|
|
|
|
|
|
item={item}
|
2026-07-13 13:51:13 +00:00
|
|
|
|
permissions={permissions}
|
2026-08-11 10:29:41 +00:00
|
|
|
|
refetch={refetch}
|
|
|
|
|
|
onIndent={handleIndent}
|
|
|
|
|
|
onOutdent={handleOutdent}
|
|
|
|
|
|
onToggleExpand={onToggleExpand}
|
2026-07-13 13:51:13 +00:00
|
|
|
|
>
|
2026-08-11 10:29:41 +00:00
|
|
|
|
{children.length > 0 && (
|
2026-02-24 20:44:16 +00:00
|
|
|
|
<SortableContext
|
2026-08-11 10:29:41 +00:00
|
|
|
|
items={children.map((child) => child.id ?? '').filter(Boolean)}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
strategy={verticalListSortingStrategy}
|
|
|
|
|
|
>
|
2026-08-11 10:29:41 +00:00
|
|
|
|
<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>
|
|
|
|
|
|
)}
|
2026-08-11 10:29:41 +00:00
|
|
|
|
</SortableMenuRow>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 10:29:41 +00:00
|
|
|
|
const rootIds = useMemo(
|
|
|
|
|
|
() => items.map((item) => item.id ?? '').filter(Boolean),
|
|
|
|
|
|
[items],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
|
return (
|
|
|
|
|
|
<DndContext
|
|
|
|
|
|
collisionDetection={closestCenter}
|
2026-08-11 10:29:41 +00:00
|
|
|
|
sensors={sensors}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
onDragStart={handleDragStart}
|
|
|
|
|
|
onDragEnd={handleDragEnd}
|
2026-08-11 10:29:41 +00:00
|
|
|
|
onDragCancel={() => setActiveItem(null)}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
>
|
2026-08-11 10:29:41 +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>
|
2026-08-11 10:29:41 +00:00
|
|
|
|
{activeItem && (
|
2026-05-19 20:22:25 +00:00
|
|
|
|
<div className="bg-white dark:bg-gray-800 rounded-md shadow-lg">
|
|
|
|
|
|
<MenuItemComponent
|
2026-08-11 10:29:41 +00:00
|
|
|
|
isDragOverlay
|
2026-05-19 20:22:25 +00:00
|
|
|
|
depth={0}
|
2026-08-11 10:29:41 +00:00
|
|
|
|
isDesignMode={isDesignMode}
|
|
|
|
|
|
item={activeItem}
|
2026-05-19 20:22:25 +00:00
|
|
|
|
permissions={permissions}
|
2026-08-11 10:29:41 +00:00
|
|
|
|
refetch={refetch}
|
2026-05-19 20:22:25 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-08-11 10:29:41 +00:00
|
|
|
|
)}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</DragOverlay>
|
|
|
|
|
|
</DndContext>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|