menu manage düzenlemeleri
This commit is contained in:
parent
35e4957cc3
commit
51b167cc6b
5 changed files with 371 additions and 157 deletions
|
|
@ -82,7 +82,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
|
||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.hld5cocdcl"
|
"revision": "0.uun45k3p9s"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
|
|
||||||
46
ui/package-lock.json
generated
46
ui/package-lock.json
generated
|
|
@ -3742,11 +3742,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -4370,9 +4369,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
|
|
@ -6171,9 +6170,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-n/node_modules/brace-expansion": {
|
"node_modules/eslint-plugin-n/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
|
|
@ -6642,9 +6641,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/filelist/node_modules/brace-expansion": {
|
"node_modules/filelist/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
|
|
@ -10421,10 +10420,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/readdir-glob/node_modules/brace-expansion": {
|
"node_modules/readdir-glob/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -11991,10 +11989,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sucrase/node_modules/brace-expansion": {
|
"node_modules/sucrase/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -12825,11 +12822,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.4.15",
|
"version": "5.4.19",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.15.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
|
||||||
"integrity": "sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==",
|
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,29 @@
|
||||||
import React from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useSortable } from '@dnd-kit/sortable'
|
import { useSortable } from '@dnd-kit/sortable'
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import * as Icons from 'lucide-react'
|
import * as Icons from 'lucide-react'
|
||||||
import { MenuItem } from '@/@types/menu'
|
import { MenuItem } from '@/@types/menu'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import { MenuService } from '@/services/menu.service'
|
||||||
|
import { MenuDto } from '@/proxy/menus'
|
||||||
|
import navigationIcon from '@/configs/navigation-icon.config'
|
||||||
|
import { FaQuestionCircle } from 'react-icons/fa'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
FormContainer,
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
Switcher,
|
||||||
|
Notification,
|
||||||
|
toast,
|
||||||
|
Select,
|
||||||
|
} from '@/components/ui'
|
||||||
|
import { Field, FieldProps, Form, Formik } from 'formik'
|
||||||
|
import { SelectBoxOption } from '@/shared/types'
|
||||||
|
import { getPermissionsList } from '@/proxy/admin/identity.service'
|
||||||
|
import { PermissionDefinitionRecord } from '@/proxy/admin'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
|
||||||
interface MenuItemComponentProps {
|
interface MenuItemComponentProps {
|
||||||
item: MenuItem
|
item: MenuItem
|
||||||
|
|
@ -11,6 +31,7 @@ interface MenuItemComponentProps {
|
||||||
depth: number
|
depth: number
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
isDragOverlay?: boolean
|
isDragOverlay?: boolean
|
||||||
|
refetch: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MenuItemComponent: React.FC<MenuItemComponentProps> = ({
|
export const MenuItemComponent: React.FC<MenuItemComponentProps> = ({
|
||||||
|
|
@ -19,7 +40,9 @@ export const MenuItemComponent: React.FC<MenuItemComponentProps> = ({
|
||||||
depth,
|
depth,
|
||||||
children,
|
children,
|
||||||
isDragOverlay = false,
|
isDragOverlay = false,
|
||||||
|
refetch,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [permissions, setPermissions] = useState<SelectBoxOption[]>([])
|
||||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||||
id: item.id || '',
|
id: item.id || '',
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -29,8 +52,20 @@ export const MenuItemComponent: React.FC<MenuItemComponentProps> = ({
|
||||||
disabled: !isDesignMode,
|
disabled: !isDesignMode,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { translate } = useLocalization()
|
const validationSchema = Yup.object().shape({
|
||||||
|
code: Yup.string().required('Code is required'),
|
||||||
|
displayName: Yup.string().required('Display Name is required'),
|
||||||
|
order: Yup.number().typeError('Order must be a number').required('Order is required'),
|
||||||
|
url: Yup.string().nullable(),
|
||||||
|
icon: Yup.string().nullable(),
|
||||||
|
cssClass: Yup.string().nullable(),
|
||||||
|
requiredPermissionName: Yup.string().nullable(),
|
||||||
|
target: Yup.string().nullable(),
|
||||||
|
elementId: Yup.string().nullable(),
|
||||||
|
isDisabled: Yup.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { translate } = useLocalization()
|
||||||
const style = {
|
const style = {
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
|
|
@ -42,14 +77,59 @@ export const MenuItemComponent: React.FC<MenuItemComponentProps> = ({
|
||||||
return IconComponent ? <IconComponent size={16} /> : <Icons.FileText size={16} />
|
return IconComponent ? <IconComponent size={16} /> : <Icons.FileText size={16} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const [isExpanded, setIsExpanded] = React.useState(true)
|
const [isExpanded, setIsExpanded] = useState(true)
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
|
const [formData, setFormData] = useState<Partial<MenuDto>>({
|
||||||
|
code: '',
|
||||||
|
displayName: '',
|
||||||
|
order: (item.children?.length || 0) + 1,
|
||||||
|
parentCode: item.code,
|
||||||
|
url: '',
|
||||||
|
icon: '',
|
||||||
|
cssClass: '',
|
||||||
|
requiredPermissionName: '',
|
||||||
|
target: '',
|
||||||
|
isDisabled: false,
|
||||||
|
elementId: '',
|
||||||
|
})
|
||||||
|
|
||||||
const toggleExpanded = () => {
|
const toggleExpanded = () => {
|
||||||
if (!isDesignMode) {
|
if (!isDesignMode) setIsExpanded(!isExpanded)
|
||||||
setIsExpanded(!isExpanded)
|
}
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
const menuService = new MenuService()
|
||||||
|
await menuService.create(formData as MenuDto)
|
||||||
|
setIsModalOpen(false)
|
||||||
|
refetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const confirmed = window.confirm(`Delete "${item.displayName}"?`)
|
||||||
|
if (!confirmed) return
|
||||||
|
const menuService = new MenuService()
|
||||||
|
await menuService.delete(item.id!)
|
||||||
|
refetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPermissionList = async () => {
|
||||||
|
const response = await getPermissionsList()
|
||||||
|
if (response.data) {
|
||||||
|
setPermissions(
|
||||||
|
response.data.map((permission: PermissionDefinitionRecord) => ({
|
||||||
|
value: permission.name,
|
||||||
|
label: permission.name,
|
||||||
|
})),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (permissions) {
|
||||||
|
getPermissionList()
|
||||||
|
}
|
||||||
|
}, [permissions])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="select-none">
|
<div className="select-none">
|
||||||
<div
|
<div
|
||||||
|
|
@ -64,13 +144,26 @@ export const MenuItemComponent: React.FC<MenuItemComponentProps> = ({
|
||||||
`}
|
`}
|
||||||
{...(isDesignMode ? { ...attributes, ...listeners } : { onClick: toggleExpanded })}
|
{...(isDesignMode ? { ...attributes, ...listeners } : { onClick: toggleExpanded })}
|
||||||
>
|
>
|
||||||
|
{isDesignMode && (
|
||||||
|
<div className="flex gap-2 items-center mr-2">
|
||||||
|
<button onClick={() => setIsModalOpen(true)} title="New Item">
|
||||||
|
<Icons.Plus size={16} className="text-green-600 hover:text-green-800" />
|
||||||
|
</button>
|
||||||
|
<button onClick={handleDelete} title="Delete Item">
|
||||||
|
<Icons.Trash2 size={16} className="text-red-600 hover:text-red-800" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
<div className="flex-shrink-0 text-gray-600">{item.icon && getIcon(item.icon)}</div>
|
<div className="flex-shrink-0 text-gray-600 text-xl">
|
||||||
|
{navigationIcon[item.icon || ''] ?? <FaQuestionCircle className="text-gray-400" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className={`
|
className={`
|
||||||
truncate text-gray-800 leading-6 text-sm
|
truncate text-gray-800 leading-6 text-sm
|
||||||
${isDesignMode ? 'font-normal' : item.children && item.children.length > 0 ? 'font-semibold' : 'font-normal'}
|
${item.children && item.children.length > 0 ? 'font-semibold' : 'font-normal'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{translate('::' + item.displayName)}
|
{translate('::' + item.displayName)}
|
||||||
|
|
@ -95,6 +188,147 @@ export const MenuItemComponent: React.FC<MenuItemComponentProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{children && (!isDesignMode ? isExpanded : true) && <div className="mt-1">{children}</div>}
|
{children && (!isDesignMode ? isExpanded : true) && <div className="mt-1">{children}</div>}
|
||||||
|
|
||||||
|
{isModalOpen && (
|
||||||
|
<Dialog
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
onRequestClose={() => setIsModalOpen(false)}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
<h5 className="mb-4">{translate('::New Item')}</h5>
|
||||||
|
<Formik
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
initialValues={formData}
|
||||||
|
enableReinitialize
|
||||||
|
onSubmit={async (values, { setSubmitting }) => {
|
||||||
|
try {
|
||||||
|
const menuService = new MenuService()
|
||||||
|
await menuService.create(values as MenuDto)
|
||||||
|
toast.push(
|
||||||
|
<Notification title="Başarılı" type="success">
|
||||||
|
{translate('::KayitEklendi')}
|
||||||
|
</Notification>,
|
||||||
|
{ placement: 'top-center' },
|
||||||
|
)
|
||||||
|
setIsModalOpen(false)
|
||||||
|
refetch()
|
||||||
|
} catch (error) {
|
||||||
|
toast.push(
|
||||||
|
<Notification title="Hata" type="danger">
|
||||||
|
{translate('::IslemBasarisiz')}
|
||||||
|
</Notification>,
|
||||||
|
{ placement: 'top-center' },
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ values, errors, touched, isSubmitting, handleChange }) => (
|
||||||
|
<Form>
|
||||||
|
<FormContainer>
|
||||||
|
<FormItem label="Code *" className="mb-2">
|
||||||
|
<Field type="text" au name="code" component={Input} className="h-8 text-sm px-2" autoFocus />
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem label="Display Name *" className="mb-2">
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
name="displayName"
|
||||||
|
component={Input}
|
||||||
|
className="h-8 text-sm px-2"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem label="Order *" className="mb-2">
|
||||||
|
<Field
|
||||||
|
type="number"
|
||||||
|
name="order"
|
||||||
|
component={Input}
|
||||||
|
className="h-8 text-sm px-2"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem label="URL" className="mb-2">
|
||||||
|
<Field type="text" name="url" component={Input} className="h-8 text-sm px-2" />
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem label="Icon" className="mb-2">
|
||||||
|
<Field type="text" name="icon" component={Input} className="h-8 text-sm px-2" />
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem label="Parent Code" className="mb-2">
|
||||||
|
<Input disabled value={item.code} className="h-8 text-sm px-2 bg-gray-100" />
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem label="CSS Class" className="mb-2">
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
name="cssClass"
|
||||||
|
component={Input}
|
||||||
|
className="h-8 text-sm px-2"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem label="Permission Name" className="mb-2">
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
name="requiredPermissionName"
|
||||||
|
className="h-8 text-sm px-2"
|
||||||
|
>
|
||||||
|
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
||||||
|
<Select
|
||||||
|
field={field}
|
||||||
|
form={form}
|
||||||
|
isClearable={true}
|
||||||
|
options={permissions}
|
||||||
|
value={permissions?.filter(
|
||||||
|
(option) => option.value === values.requiredPermissionName,
|
||||||
|
)}
|
||||||
|
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem label="Target" className="mb-2">
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
name="target"
|
||||||
|
component={Input}
|
||||||
|
className="h-8 text-sm px-2"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem label="Element Id" className="mb-2">
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
name="elementId"
|
||||||
|
component={Input}
|
||||||
|
className="h-8 text-sm px-2"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem label="Is Disabled" className="mb-2">
|
||||||
|
<Field name="isDisabled" component={Switcher} />
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 mt-4">
|
||||||
|
<Button variant="plain" size="sm" onClick={() => setIsModalOpen(false)}>
|
||||||
|
{translate('::Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" variant="solid" size="sm" loading={isSubmitting}>
|
||||||
|
{translate('::Create')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormContainer>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { useMenuData } from '@/utils/hooks/useMenuData'
|
||||||
|
|
||||||
export const MenuManager = () => {
|
export const MenuManager = () => {
|
||||||
const { menuItems, setMenuItems, loading, error, refetch, saveMenuData } = useMenuData()
|
const { menuItems, setMenuItems, loading, error, refetch, saveMenuData } = useMenuData()
|
||||||
const [isDesignMode, setIsDesignMode] = useState(false)
|
const [isDesignMode, setIsDesignMode] = useState(true)
|
||||||
const [isSaving, setIsSaving] = useState(false)
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
const [saveMessage, setSaveMessage] = useState<{
|
const [saveMessage, setSaveMessage] = useState<{
|
||||||
type: 'success' | 'error'
|
type: 'success' | 'error'
|
||||||
|
|
@ -27,6 +27,7 @@ export const MenuManager = () => {
|
||||||
await saveMenuData(menuItems)
|
await saveMenuData(menuItems)
|
||||||
setSaveMessage({ type: 'success', text: 'Menu configuration saved successfully!' })
|
setSaveMessage({ type: 'success', text: 'Menu configuration saved successfully!' })
|
||||||
setTimeout(() => setSaveMessage(null), 3000)
|
setTimeout(() => setSaveMessage(null), 3000)
|
||||||
|
setIsDesignMode(false)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setSaveMessage({
|
setSaveMessage({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
|
@ -75,82 +76,81 @@ export const MenuManager = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="w-full h-full">
|
||||||
<div className="w-full h-full p-6">
|
{/* Menu Tree */}
|
||||||
{/* Menu Tree */}
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="flex items-center justify-between mb-6 flex-wrap gap-4">
|
||||||
<div className="flex items-center justify-between mb-6 flex-wrap gap-4">
|
{/* Sol kısım: Başlık */}
|
||||||
{/* Sol kısım: Başlık */}
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<Icons.Menu size={20} className="text-gray-600" />
|
||||||
<Icons.Menu size={20} className="text-gray-600" />
|
<h2 className="text-lg font-semibold text-gray-900">Menu Manager</h2>
|
||||||
<h2 className="text-lg font-semibold text-gray-900">Menu Manager</h2>
|
<span className="text-sm text-gray-500">({menuItems.length} root items)</span>
|
||||||
<span className="text-sm text-gray-500">({menuItems.length} root items)</span>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sağ kısım: Design Mode + Save butonu */}
|
{/* Sağ kısım: Design Mode + Save butonu */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-medium ${isDesignMode ? 'text-blue-600' : 'text-gray-500'}`}
|
className={`text-sm font-medium ${isDesignMode ? 'text-blue-600' : 'text-gray-500'}`}
|
||||||
>
|
>
|
||||||
Design Mode
|
Design Mode
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={handleToggleDesignMode}
|
onClick={handleToggleDesignMode}
|
||||||
className={`
|
className={`
|
||||||
relative inline-flex h-6 w-11 items-center rounded-full transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
relative inline-flex h-6 w-11 items-center rounded-full transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
||||||
${isDesignMode ? 'bg-blue-600' : 'bg-gray-200'}
|
${isDesignMode ? 'bg-blue-600' : 'bg-gray-200'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`
|
className={`
|
||||||
inline-block h-4 w-4 transform rounded-full bg-white transition-transform duration-200 ease-in-out
|
inline-block h-4 w-4 transform rounded-full bg-white transition-transform duration-200 ease-in-out
|
||||||
${isDesignMode ? 'translate-x-6' : 'translate-x-1'}
|
${isDesignMode ? 'translate-x-6' : 'translate-x-1'}
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={!isDesignMode || isSaving}
|
disabled={!isDesignMode || isSaving}
|
||||||
className={`
|
className={`
|
||||||
flex items-center gap-2 px-4 py-2 rounded-lg transition-colors
|
flex items-center gap-2 px-4 py-2 rounded-lg transition-colors
|
||||||
${isDesignMode ? 'bg-green-600 hover:bg-green-700 text-white' : 'bg-gray-300 text-gray-500 cursor-not-allowed'}
|
${isDesignMode ? 'bg-green-600 hover:bg-green-700 text-white' : 'bg-gray-300 text-gray-500 cursor-not-allowed'}
|
||||||
${isSaving ? 'opacity-50' : ''}
|
${isSaving ? 'opacity-50' : ''}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{isSaving ? (
|
{isSaving ? (
|
||||||
<>
|
<>
|
||||||
<Icons.Loader2 size={16} className="animate-spin" />
|
<Icons.Loader2 size={16} className="animate-spin" />
|
||||||
Saving...
|
Saving...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Icons.Save size={16} />
|
<Icons.Save size={16} />
|
||||||
Save Changes
|
Save Changes
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{menuItems.length > 0 ? (
|
|
||||||
<SortableMenuTree
|
|
||||||
items={menuItems}
|
|
||||||
onItemsChange={handleMenuChange}
|
|
||||||
isDesignMode={isDesignMode}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-12 text-gray-500">
|
|
||||||
<Icons.Menu size={24} className="mx-auto mb-4 text-gray-300" />
|
|
||||||
<p className="text-lg">No menu items found</p>
|
|
||||||
<p className="text-sm">Try refreshing the page or contact your administrator</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{menuItems.length > 0 ? (
|
||||||
|
<SortableMenuTree
|
||||||
|
items={menuItems}
|
||||||
|
onItemsChange={handleMenuChange}
|
||||||
|
isDesignMode={isDesignMode}
|
||||||
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12 text-gray-500">
|
||||||
|
<Icons.Menu size={24} className="mx-auto mb-4 text-gray-300" />
|
||||||
|
<p className="text-lg">No menu items found</p>
|
||||||
|
<p className="text-sm">Try refreshing the page or contact your administrator</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,14 @@ interface SortableMenuTreeProps {
|
||||||
items: MenuItem[]
|
items: MenuItem[]
|
||||||
onItemsChange: (items: MenuItem[]) => void
|
onItemsChange: (items: MenuItem[]) => void
|
||||||
isDesignMode: boolean
|
isDesignMode: boolean
|
||||||
|
refetch: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SortableMenuTree: React.FC<SortableMenuTreeProps> = ({
|
export const SortableMenuTree: React.FC<SortableMenuTreeProps> = ({
|
||||||
items,
|
items,
|
||||||
onItemsChange,
|
onItemsChange,
|
||||||
isDesignMode,
|
isDesignMode,
|
||||||
|
refetch,
|
||||||
}) => {
|
}) => {
|
||||||
const [activeItem, setActiveItem] = React.useState<MenuItem | null>(null)
|
const [activeItem, setActiveItem] = React.useState<MenuItem | null>(null)
|
||||||
|
|
||||||
|
|
@ -42,31 +44,25 @@ export const SortableMenuTree: React.FC<SortableMenuTreeProps> = ({
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Flatten the tree structure to get all items with their paths
|
|
||||||
const flattenItems = (
|
const flattenItems = (
|
||||||
items: MenuItem[],
|
items: MenuItem[],
|
||||||
parentPath: string[] = [],
|
parentPath: number[] = [],
|
||||||
): Array<{ item: MenuItem; path: number[] }> => {
|
): Array<{ item: MenuItem; path: number[] }> => {
|
||||||
const result: Array<{ item: MenuItem; path: number[] }> = []
|
return items.flatMap((item, index) => {
|
||||||
|
const path = [...parentPath, index]
|
||||||
|
const self = { item, path }
|
||||||
|
|
||||||
items.forEach((item, index) => {
|
if (item.children?.length) {
|
||||||
const currentPath = [...parentPath, index]
|
return [self, ...flattenItems(item.children, path)]
|
||||||
result.push({ item, path: currentPath })
|
|
||||||
|
|
||||||
if (item.children && item.children.length > 0) {
|
|
||||||
result.push(...flattenItems(item.children, currentPath))
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
return result
|
return [self]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find item by ID in the tree
|
|
||||||
const findItemById = (items: MenuItem[], id: string): MenuItem | null => {
|
const findItemById = (items: MenuItem[], id: string): MenuItem | null => {
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.id === id) {
|
if (item.id === id) return item
|
||||||
return item
|
|
||||||
}
|
|
||||||
if (item.children) {
|
if (item.children) {
|
||||||
const found = findItemById(item.children, id)
|
const found = findItemById(item.children, id)
|
||||||
if (found) return found
|
if (found) return found
|
||||||
|
|
@ -75,12 +71,9 @@ export const SortableMenuTree: React.FC<SortableMenuTreeProps> = ({
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove item from tree by ID
|
|
||||||
const removeItemFromTree = (items: MenuItem[], id: string): MenuItem[] => {
|
const removeItemFromTree = (items: MenuItem[], id: string): MenuItem[] => {
|
||||||
return items.reduce((acc: MenuItem[], item) => {
|
return items.reduce((acc: MenuItem[], item) => {
|
||||||
if (item.id === id) {
|
if (item.id === id) return acc
|
||||||
return acc // Skip this item (remove it)
|
|
||||||
}
|
|
||||||
|
|
||||||
const newItem = { ...item }
|
const newItem = { ...item }
|
||||||
if (newItem.children && newItem.children.length > 0) {
|
if (newItem.children && newItem.children.length > 0) {
|
||||||
|
|
@ -92,7 +85,6 @@ export const SortableMenuTree: React.FC<SortableMenuTreeProps> = ({
|
||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert item at specific position
|
|
||||||
const insertItemAtPath = (
|
const insertItemAtPath = (
|
||||||
items: MenuItem[],
|
items: MenuItem[],
|
||||||
item: MenuItem,
|
item: MenuItem,
|
||||||
|
|
@ -117,25 +109,28 @@ export const SortableMenuTree: React.FC<SortableMenuTreeProps> = ({
|
||||||
return newItems
|
return newItems
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the path where an item should be inserted based on over item
|
|
||||||
const getInsertionPath = (
|
const getInsertionPath = (
|
||||||
items: MenuItem[],
|
items: MenuItem[],
|
||||||
activeId: string,
|
activeId: string,
|
||||||
overId: string,
|
overId: string,
|
||||||
): number[] | null => {
|
): number[] | null => {
|
||||||
const flatItems = flattenItems(items)
|
const flat = flattenItems(items)
|
||||||
const activeIndex = flatItems.findIndex(({ item }) => item.id === activeId)
|
const activeFlat = flat.find((f) => f.item.id === activeId)
|
||||||
const overIndex = flatItems.findIndex(({ item }) => item.id === overId)
|
const overFlat = flat.find((f) => f.item.id === overId)
|
||||||
|
|
||||||
if (overIndex === -1) return null
|
if (!activeFlat || !overFlat) return null
|
||||||
|
|
||||||
const overItem = flatItems[overIndex]
|
const isSameParent = activeFlat.path.slice(0, -1).join() === overFlat.path.slice(0, -1).join()
|
||||||
const insertPath = [...overItem.path]
|
|
||||||
|
|
||||||
// Aktif item, listedeki over item'den sonra geliyorsa, yukarı taşınıyordur → over item'ın yerine ekle
|
const insertPath = [...overFlat.path]
|
||||||
// Aktif item, listedeki over item'den önceyse, aşağı taşınıyordur → bir SONRASINA ekle ki yeniden aynı yere düşmesin
|
|
||||||
if (activeIndex < overIndex) {
|
if (isSameParent) {
|
||||||
insertPath[insertPath.length - 1] += 1
|
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
|
return insertPath
|
||||||
|
|
@ -159,50 +154,43 @@ export const SortableMenuTree: React.FC<SortableMenuTreeProps> = ({
|
||||||
const { active, over } = event
|
const { active, over } = event
|
||||||
setActiveItem(null)
|
setActiveItem(null)
|
||||||
|
|
||||||
if (!over || active.id === over.id || !isDesignMode) {
|
if (!over || active.id === over.id || !isDesignMode) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeId = active.id as string
|
const activeId = active.id as string
|
||||||
const overId = over.id as string
|
const overId = over.id as string
|
||||||
|
|
||||||
const activeItem = findItemById(items, activeId)
|
const activeItem = findItemById(items, activeId)
|
||||||
if (!activeItem) return
|
if (!activeItem) return
|
||||||
|
|
||||||
// ⛳️ Kullanılması gereken liste: `items`
|
|
||||||
const insertionPath = getInsertionPath(items, activeId, overId)
|
const insertionPath = getInsertionPath(items, activeId, overId)
|
||||||
if (!insertionPath) return
|
if (!insertionPath) return
|
||||||
|
|
||||||
// Şimdi aktif elemanı çıkar
|
|
||||||
let newItems = removeItemFromTree(items, activeId)
|
let newItems = removeItemFromTree(items, activeId)
|
||||||
|
|
||||||
// ve hedef konuma ekle
|
|
||||||
newItems = insertItemAtPath(newItems, activeItem, insertionPath)
|
newItems = insertItemAtPath(newItems, activeItem, insertionPath)
|
||||||
|
|
||||||
// Sıra numaralarını güncelle
|
|
||||||
const finalItems = updateOrderNumbers(newItems)
|
const finalItems = updateOrderNumbers(newItems)
|
||||||
onItemsChange(finalItems)
|
onItemsChange(finalItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderMenuItem = (item: MenuItem, depth: number = 0): React.ReactNode => {
|
const renderMenuItem = (item: MenuItem, depth: number = 0): React.ReactNode => {
|
||||||
return (
|
return (
|
||||||
<MenuItemComponent
|
<div key={item.id}>
|
||||||
key={item.id || `temp-${Math.random().toString(36).substr(2, 9)}`}
|
<MenuItemComponent item={item} isDesignMode={isDesignMode} depth={depth} refetch={refetch}>
|
||||||
item={item}
|
{Array.isArray(item.children) && item.children.length > 0 && (
|
||||||
isDesignMode={isDesignMode}
|
<SortableContext
|
||||||
depth={depth}
|
items={item.children
|
||||||
>
|
.filter((child): child is MenuItem & { id: string } => !!child.id)
|
||||||
{item.children && item.children.length > 0 && (
|
.map((child) => child.id)}
|
||||||
<div className="ml-4">
|
strategy={verticalListSortingStrategy}
|
||||||
{item.children.map((child) => renderMenuItem(child, depth + 1))}
|
>
|
||||||
</div>
|
<div className="ml-4">
|
||||||
)}
|
{item.children.map((child) => renderMenuItem(child, depth + 1))}
|
||||||
</MenuItemComponent>
|
</div>
|
||||||
|
</SortableContext>
|
||||||
|
)}
|
||||||
|
</MenuItemComponent>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const allItems = flattenItems(items)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
|
|
@ -210,12 +198,7 @@ export const SortableMenuTree: React.FC<SortableMenuTreeProps> = ({
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
<SortableContext
|
<div className="space-y-1">{items.map((item) => renderMenuItem(item))}</div>
|
||||||
items={allItems.map(({ item }) => item.id)}
|
|
||||||
strategy={verticalListSortingStrategy}
|
|
||||||
>
|
|
||||||
<div className="space-y-1">{items.map((item) => renderMenuItem(item))}</div>
|
|
||||||
</SortableContext>
|
|
||||||
|
|
||||||
<DragOverlay>
|
<DragOverlay>
|
||||||
{activeItem ? (
|
{activeItem ? (
|
||||||
|
|
@ -224,6 +207,7 @@ export const SortableMenuTree: React.FC<SortableMenuTreeProps> = ({
|
||||||
isDesignMode={isDesignMode}
|
isDesignMode={isDesignMode}
|
||||||
depth={0}
|
depth={0}
|
||||||
isDragOverlay={true}
|
isDragOverlay={true}
|
||||||
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue